fix: prevent stale voice sidebar after multi-session identity collision

When a second browser joins voice, LiveKit disconnects the first browser
via identity collision. The first browser's leaveVoice() was optimistically
removing the user from voiceUsers, but the user is still in voice from
the other session. Add handleForceDisconnect() that clears local connection
state without touching voiceUsers, keeping the sidebar accurate.

Also fix deploy.sh to clean up stale renamed containers from failed recreates.
This commit is contained in:
Jannis Braun
2026-03-09 23:48:44 +01:00
parent cfd24c5d58
commit 2999ca95a0
3 changed files with 27 additions and 2 deletions
+1 -1
View File
@@ -491,7 +491,7 @@ export function useLiveKit() {
// → clear voice intent so AppLayout doesn't auto-retry into an infinite loop.
// Client-initiated disconnects already clear this via leaveVoice() / VoiceControlBar.
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
useVoiceStore.getState().leaveVoice();
useVoiceStore.getState().handleForceDisconnect();
}
});
+24
View File
@@ -97,6 +97,7 @@ interface VoiceState {
clearAllVoiceUsers: () => void;
clearVoiceUsersForOrigin: (origin: string) => void;
leaveVoice: () => void;
handleForceDisconnect: () => void;
reset: () => void;
}
@@ -370,6 +371,29 @@ export const useVoiceStore = create<VoiceState>()(
});
},
// Force disconnect: clear local connection state but do NOT touch voiceUsers.
// Used for involuntary disconnects (identity collision, server shutdown, etc.)
// where the server is the authority on who's actually in voice.
handleForceDisconnect: () => {
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(),
});
},
reset: () => set({
voiceUsers: new Map(),
currentVoiceChannelId: null,