diff --git a/packages/web/src/components/layout/MobileVoiceFullScreen.tsx b/packages/web/src/components/layout/MobileVoiceFullScreen.tsx index a5f4f950..898afcc3 100644 --- a/packages/web/src/components/layout/MobileVoiceFullScreen.tsx +++ b/packages/web/src/components/layout/MobileVoiceFullScreen.tsx @@ -23,6 +23,10 @@ export function MobileVoiceFullScreen() { const leaveVoice = useVoiceStore((s) => s.leaveVoice); const voiceUsers = useVoiceStore((s) => s.voiceUsers); const voiceUserStates = useVoiceStore((s) => s.voiceUserStates); + const participants = useVoiceStore((s) => s.participants); + const spaceMutedUserIds = useVoiceStore((s) => s.spaceMutedUserIds); + const spaceDeafenedUserIds = useVoiceStore((s) => s.spaceDeafenedUserIds); + const permissionMutedUserIds = useVoiceStore((s) => s.permissionMutedUserIds); const channels = useSpaceStore((s) => s.channels); const dmChannels = useSpaceStore((s) => s.dmChannels); @@ -67,7 +71,7 @@ export function MobileVoiceFullScreen() { const member = members.find(m => m.userId === userId); if (member) { return { - name: member.nickname ?? member.user?.displayName ?? member.user?.username ?? 'User', + name: member.nickname ?? member.user?.displayName ?? member.user?.username ?? userId, avatar: member.user?.avatar ? `/api/uploads/${member.user.avatar}` : null, avatarColor: member.user?.avatarColor ?? null, }; @@ -83,7 +87,12 @@ export function MobileVoiceFullScreen() { }; } } - return { name: 'User', avatar: null, avatarColor: null }; + // Fall back to LiveKit participant metadata + const participant = participants.find(p => p.userId === userId); + if (participant?.username) { + return { name: participant.username, avatar: null, avatarColor: null }; + } + return { name: userId, avatar: null, avatarColor: null }; }; const handleDisconnect = () => { @@ -159,9 +168,31 @@ export function MobileVoiceFullScreen() { }`}> {participantIds.map(userId => { const info = getParticipantInfo(userId); - const state = voiceUserStates.get(userId); + const wsStatus = voiceUserStates.get(userId); + const participant = participants.find(p => p.userId === userId); const isMe = userId === authUser?.id; + // Resolve mute/deafen: local state for self, LiveKit participant then WS fallback for others + const isUserMuted = isMe + ? isMuted + : (participant?.isMuted ?? wsStatus?.isMuted ?? false); + const isUserDeafened = isMe + ? isDeafened + : (participant?.isDeafened ?? wsStatus?.isDeafened ?? false); + + // Server-enforced states + const spaceId = !isDmCall && currentVoiceChannelId + ? channelToSpaceMap.get(currentVoiceChannelId) + : undefined; + const isSpaceMuted = spaceId ? spaceMutedUserIds.has(`${spaceId}:${userId}`) : false; + const isSpaceDeafened = spaceId ? spaceDeafenedUserIds.has(`${spaceId}:${userId}`) : false; + const isPermissionMuted = spaceId ? permissionMutedUserIds.has(`${spaceId}:${userId}`) : false; + + // Any muted indicator: self-mute, server mute, or permission mute + const showMuted = isUserMuted || isSpaceMuted || isPermissionMuted; + // Any deafened indicator: self-deafen or server deafen + const showDeafened = isUserDeafened || isSpaceDeafened; + return (
- {state?.isMuted && ( + {(showMuted || showDeafened) && (