fix: optimistic voice state so user appears in sidebar immediately

Previously, the voice channel sidebar only showed users after a server
round-trip (voice_state_update broadcast). After a deploy/reconnect,
this left the user invisible in the sidebar despite being connected.
Now joinVoiceChannel, leaveVoice, and the WS ready handler all
optimistically update voiceUsers for the local user immediately.
This commit is contained in:
Jannis Braun
2026-03-04 21:48:39 +01:00
parent a5ea7633fc
commit a5073f89fd
3 changed files with 46 additions and 18 deletions
+3
View File
@@ -172,6 +172,9 @@ function handleEvent(origin: string, event: ServerEvent): void {
console.log('[WebSocket] Re-syncing voice status on reconnect:', { currentVoiceChannelId, origin, curMuted, curDeafened, curCamera, curScreen });
wsSend({ type: 'voice_join', channelId: currentVoiceChannelId }, origin);
wsSend({ type: 'voice_status', isMuted: curMuted, isDeafened: curDeafened, isCameraOn: curCamera, isScreenSharing: curScreen }, origin);
// Optimistic: immediately show self in voice channel sidebar
const myId = isHome ? event.user.id : useAuthStore.getState().user?.id;
if (myId) addVoiceUser(currentVoiceChannelId, myId);
}
}
}
+35 -17
View File
@@ -3,6 +3,7 @@ import { persist, createJSONStorage } from 'zustand/middleware';
import type { ParticipantInfo } from '../hooks/useLiveKit';
import { AudioManager } from '../audio/AudioManager';
import { useServerStore } from './serverStore';
import { useAuthStore } from './authStore';
export interface ScreenShareConfig {
height: 1080 | 720 | 540;
@@ -291,23 +292,40 @@ export const useVoiceStore = create<VoiceState>()(
},
// Leave voice without wiping the voiceUsers map (so sidebar still shows others)
leaveVoice: () => set({
currentVoiceChannelId: null,
isCameraOn: false,
isScreenSharing: false,
participants: [],
speakingParticipantIds: new Set(),
connectionError: null,
isLiveKitConnected: false,
connectionQuality: 'unknown',
focusedParticipantId: null,
activeDmCall: null,
outgoingCall: null,
deafenedUserIds: new Set(),
streamVolumes: new Map(),
streamMutes: new Map(),
watchingStreams: new Set(),
}),
leaveVoice: () => {
const channelId = get().currentVoiceChannelId;
const myId = useAuthStore.getState().user?.id;
set((state) => {
// Optimistic: immediately remove self from the channel's voice users
const voiceUsers = (channelId && myId)
? (() => {
const m = new Map(state.voiceUsers);
m.set(channelId, (m.get(channelId) ?? []).filter(id => id !== myId));
return m;
})()
: state.voiceUsers;
return {
currentVoiceChannelId: null,
isCameraOn: false,
isScreenSharing: false,
participants: [],
speakingParticipantIds: new Set(),
connectionError: null,
isLiveKitConnected: false,
connectionQuality: 'unknown',
focusedParticipantId: null,
activeDmCall: null,
outgoingCall: null,
deafenedUserIds: new Set(),
streamVolumes: new Map(),
streamMutes: new Map(),
watchingStreams: new Set(),
voiceUsers,
};
});
},
reset: () => set({
voiceUsers: new Map(),
+8 -1
View File
@@ -1,4 +1,5 @@
import { useVoiceStore } from '../stores/voiceStore';
import { useAuthStore } from '../stores/authStore';
import { getChannelOrigin } from '../stores/serverStore';
import { wsSend } from '../hooks/useWebSocket';
@@ -9,9 +10,11 @@ import { wsSend } from '../hooks/useWebSocket';
* broadcasts a leave event and the client cleans up stale voice state.
*/
export function joinVoiceChannel(channelId: string): void {
const { currentVoiceChannelId, setCurrentVoiceChannel } = useVoiceStore.getState();
const { currentVoiceChannelId, setCurrentVoiceChannel, addVoiceUser, removeVoiceUser } = useVoiceStore.getState();
if (currentVoiceChannelId === channelId) return;
const myId = useAuthStore.getState().user?.id;
// Leave old instance if switching cross-origin
if (currentVoiceChannelId) {
const oldOrigin = getChannelOrigin(currentVoiceChannelId);
@@ -19,8 +22,12 @@ export function joinVoiceChannel(channelId: string): void {
if (oldOrigin !== newOrigin) {
wsSend({ type: 'voice_leave' }, oldOrigin);
}
// Optimistic: immediately remove self from old channel
if (myId) removeVoiceUser(currentVoiceChannelId, myId);
}
setCurrentVoiceChannel(channelId);
wsSend({ type: 'voice_join', channelId }, getChannelOrigin(channelId));
// Optimistic: immediately show self in new channel
if (myId) addVoiceUser(channelId, myId);
}