chore: show call participant mute and camera-off state (#279)

Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
This commit is contained in:
2026-05-21 20:58:53 +00:00
committed by hodlbod
parent ffd06ab561
commit 0d61278c56
7 changed files with 148 additions and 34 deletions
+40 -3
View File
@@ -8,6 +8,7 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import VideoCallTile from "@app/components/VideoCallTile.svelte"
import VoiceWidget from "@app/components/VoiceWidget.svelte"
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
import {get} from "svelte/store"
import {
VideoCallLayout,
@@ -18,7 +19,12 @@
ViewportSize,
videoPrimaryTileKey,
} from "@app/call/video"
import {currentVoiceSession, currentVoiceRoom, pubkeyFromLiveKitIdentity} from "@app/call/stores"
import {
currentVoiceSession,
currentVoiceRoom,
mediaStateByIdentity,
pubkeyFromLiveKitIdentity,
} from "@app/call/stores"
type Props = {
layout: VideoCallLayout
@@ -121,6 +127,25 @@
source: Track.Source.ScreenShare,
})
}
if (!videoTiles.some(t => t.identity === rp.identity)) {
videoTiles.push({
identity: rp.identity,
isLocal: false,
trackSid: `avatar-${rp.identity}`,
track: undefined,
source: Track.Source.Camera,
})
}
}
if (!videoTiles.some(t => t.identity === user.identity)) {
videoTiles.push({
identity: user.identity,
isLocal: true,
trackSid: "local-avatar",
track: undefined,
source: Track.Source.Camera,
})
}
return videoTiles
@@ -187,6 +212,7 @@
</script>
{#snippet videoTile(tile: VideoTileData, layout: TileLayout)}
{@const media = $mediaStateByIdentity(tile.identity)}
<div
class={cx(
"relative isolate overflow-hidden rounded-box shadow-sm",
@@ -206,6 +232,15 @@
<ProfileCircle pubkey={pubkeyFromLiveKitIdentity(tile.identity)} {url} size={14} />
</div>
{/if}
{#if tile.track}
<div class="pointer-events-none absolute left-1 top-1 z-10">
<VoiceParticipantMediaBadges
muted={media.muted}
cameraOn={media.cameraOn}
showCamera={tile.source === Track.Source.Camera}
size={3} />
</div>
{/if}
<span
class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs">
{labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""}
@@ -256,8 +291,10 @@
{:else}
<div
class="flex min-h-[12rem] flex-1 flex-col items-center justify-center gap-2 rounded-box bg-base-200/50 p-4 text-center text-sm opacity-80">
<p>No camera or screen share yet.</p>
<p class="text-xs">Use the camera or screen share control to share video.</p>
<p>No one is sharing video yet.</p>
<p class="text-xs">
Participants appear here when they turn on their camera or share their screen.
</p>
</div>
{/if}
{/snippet}
@@ -0,0 +1,34 @@
<script lang="ts">
import cx from "classnames"
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
import VideocameraOff from "@assets/icons/videocamera-off.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
type Props = {
muted: boolean
cameraOn: boolean
showCamera?: boolean
size?: number
class?: string
}
const {muted, cameraOn, showCamera = true, size = 3, class: className = ""}: Props = $props()
const badgeClass =
"inline-flex size-4 shrink-0 items-center justify-center rounded bg-base-100/80 p-0.5 text-error"
</script>
{#if muted || (showCamera && !cameraOn)}
<div class={cx("flex items-center gap-1", className)}>
{#if muted}
<span class={badgeClass} aria-label="Muted">
<Icon icon={MicrophoneOff} {size} />
</span>
{/if}
{#if showCamera && !cameraOn}
<span class={badgeClass} aria-label="Camera off">
<Icon icon={VideocameraOff} {size} />
</span>
{/if}
</div>
{/if}
+11 -1
View File
@@ -9,11 +9,13 @@
import {makeRoomPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
import {makeRoomId} from "@app/core/state"
import {
VoiceState,
currentVoiceRoom,
isParticipantSpeaking,
mediaStateByIdentity,
participantKey,
voiceState,
type VoiceParticipant,
@@ -83,9 +85,17 @@
)}>
<ProfileCircle pubkey={p.pubkey} size={5} class="h-5 w-5" />
</div>
<span class="ellipsize text-xs opacity-70">
<span class="ellipsize min-w-0 flex-1 text-xs opacity-70">
{p.pubkey ? displayProfileByPubkey(p.pubkey) : "Unknown"}
</span>
{#if isActive}
{@const media = $mediaStateByIdentity(p.identity)}
<VoiceParticipantMediaBadges
muted={media.muted}
cameraOn={media.cameraOn}
size={3}
class="shrink-0" />
{/if}
</div>
{/each}
{/if}
+11 -5
View File
@@ -6,7 +6,7 @@
import cx from "classnames"
import {displayRelayUrl} from "@welshman/util"
import Microphone from "@assets/icons/microphone.svg?dataurl"
import Videocamera from "@assets/icons/videocamera.svg?dataurl"
import VideocameraOff from "@assets/icons/videocamera-off.svg?dataurl"
import VideocameraRecord from "@assets/icons/videocamera-record.svg?dataurl"
import Monitor from "@assets/icons/monitor.svg?dataurl"
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
@@ -43,7 +43,6 @@
currentVoiceRoom,
voiceMicMuted,
voiceState,
isLocalSpeaking,
} from "@app/call/stores"
import {cancelJoinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice"
@@ -188,7 +187,6 @@
class={cx(
mediaToggleClass,
"overflow-visible",
!$voiceMicMuted && $isLocalSpeaking && "text-primary",
$voiceMicMuted && "text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
)}
onclick={toggleMute}>
@@ -207,9 +205,17 @@
</Button>
<Button
data-tip={$currentVoiceSession.cameraOn ? "Turn off camera" : "Turn on camera"}
class={cx(mediaToggleClass, $currentVoiceSession.cameraOn && "text-primary")}
class={cx(
mediaToggleClass,
"overflow-visible",
$currentVoiceSession.cameraOn && "text-primary",
!$currentVoiceSession.cameraOn &&
"text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
)}
onclick={toggleCamera}>
<Icon icon={$currentVoiceSession.cameraOn ? VideocameraRecord : Videocamera} size={4} />
<Icon
icon={$currentVoiceSession.cameraOn ? VideocameraRecord : VideocameraOff}
size={4} />
</Button>
{#if !Capacitor.isNativePlatform()}
<Button