feat: voice status visibility + sidebar persistence fixes

- Add WebSocket voice_status/voice_status_update events so mute/deafen
  icons are visible in the sidebar without joining the voice channel
- Server tracks voiceUserStates and includes them in the ready payload
- Re-register voice channel on WebSocket reconnect to prevent sidebar
  users from disappearing after idle timeout
- Re-broadcast deafen state to late joiners via LiveKit data channel
- Fix black grid tile when video stops (enabled-flag guards)
- Remove duplicate mute/deafen from VoiceControls (replaced with
  Video Quality + Noise Suppression)
- Fix missing users in sidebar voice list (identity matching + fallback)
This commit is contained in:
Jannis Braun
2026-02-19 07:58:50 +01:00
parent 503e483b82
commit 0da4a530d6
10 changed files with 307 additions and 102 deletions
+38 -1
View File
@@ -41,6 +41,14 @@ interface VoiceState {
toggleDeafen: () => void;
setFocusedParticipant: (id: string | null) => void;
setVideoQuality: (quality: '1080p' | '1080p60' | '720p' | '720p60' | '540p' | '360p') => void;
noiseSuppression: boolean;
toggleNoiseSuppression: () => void;
deafenedUserIds: Set<string>;
setUserDeafened: (userId: string, deafened: boolean) => void;
// WebSocket-based voice user status (visible without joining LiveKit)
voiceUserStates: Map<string, { isMuted: boolean; isDeafened: boolean }>;
setVoiceUserStatus: (userId: string, isMuted: boolean, isDeafened: boolean) => void;
clearVoiceUserStatus: (userId: string) => void;
getVoiceUsers: (channelId: string) => string[];
clearAllVoiceUsers: () => void;
leaveVoice: () => void;
@@ -123,10 +131,36 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
setFocusedParticipant: (id) => set({ focusedParticipantId: id }),
setVideoQuality: (quality) => set({ videoQuality: quality }),
noiseSuppression: true,
toggleNoiseSuppression: () => set((state) => ({ noiseSuppression: !state.noiseSuppression })),
deafenedUserIds: new Set(),
setUserDeafened: (userId, deafened) => {
set((state) => {
const newSet = new Set(state.deafenedUserIds);
if (deafened) newSet.add(userId); else newSet.delete(userId);
return { deafenedUserIds: newSet };
});
},
voiceUserStates: new Map(),
setVoiceUserStatus: (userId, isMuted, isDeafened) => {
set((state) => {
const newMap = new Map(state.voiceUserStates);
newMap.set(userId, { isMuted, isDeafened });
return { voiceUserStates: newMap };
});
},
clearVoiceUserStatus: (userId) => {
set((state) => {
const newMap = new Map(state.voiceUserStates);
newMap.delete(userId);
return { voiceUserStates: newMap };
});
},
getVoiceUsers: (channelId) => get().voiceUsers.get(channelId) ?? [],
clearAllVoiceUsers: () => set({ voiceUsers: new Map() }),
clearAllVoiceUsers: () => set({ voiceUsers: new Map(), voiceUserStates: new Map() }),
// Leave voice without wiping the voiceUsers map (so sidebar still shows others)
leaveVoice: () => set({
@@ -143,6 +177,7 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
focusedParticipantId: null,
activeDmCall: null,
outgoingCall: null,
deafenedUserIds: new Set(),
}),
reset: () => set({
@@ -162,5 +197,7 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
incomingCall: null,
outgoingCall: null,
activeDmCall: null,
deafenedUserIds: new Set(),
voiceUserStates: new Map(),
}),
}));