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:
@@ -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 });
|
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_join', channelId: currentVoiceChannelId }, origin);
|
||||||
wsSend({ type: 'voice_status', isMuted: curMuted, isDeafened: curDeafened, isCameraOn: curCamera, isScreenSharing: curScreen }, 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { persist, createJSONStorage } from 'zustand/middleware';
|
|||||||
import type { ParticipantInfo } from '../hooks/useLiveKit';
|
import type { ParticipantInfo } from '../hooks/useLiveKit';
|
||||||
import { AudioManager } from '../audio/AudioManager';
|
import { AudioManager } from '../audio/AudioManager';
|
||||||
import { useServerStore } from './serverStore';
|
import { useServerStore } from './serverStore';
|
||||||
|
import { useAuthStore } from './authStore';
|
||||||
|
|
||||||
export interface ScreenShareConfig {
|
export interface ScreenShareConfig {
|
||||||
height: 1080 | 720 | 540;
|
height: 1080 | 720 | 540;
|
||||||
@@ -291,7 +292,21 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Leave voice without wiping the voiceUsers map (so sidebar still shows others)
|
// Leave voice without wiping the voiceUsers map (so sidebar still shows others)
|
||||||
leaveVoice: () => 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,
|
currentVoiceChannelId: null,
|
||||||
isCameraOn: false,
|
isCameraOn: false,
|
||||||
isScreenSharing: false,
|
isScreenSharing: false,
|
||||||
@@ -307,7 +322,10 @@ export const useVoiceStore = create<VoiceState>()(
|
|||||||
streamVolumes: new Map(),
|
streamVolumes: new Map(),
|
||||||
streamMutes: new Map(),
|
streamMutes: new Map(),
|
||||||
watchingStreams: new Set(),
|
watchingStreams: new Set(),
|
||||||
}),
|
voiceUsers,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
reset: () => set({
|
reset: () => set({
|
||||||
voiceUsers: new Map(),
|
voiceUsers: new Map(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useVoiceStore } from '../stores/voiceStore';
|
import { useVoiceStore } from '../stores/voiceStore';
|
||||||
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import { getChannelOrigin } from '../stores/serverStore';
|
import { getChannelOrigin } from '../stores/serverStore';
|
||||||
import { wsSend } from '../hooks/useWebSocket';
|
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.
|
* broadcasts a leave event and the client cleans up stale voice state.
|
||||||
*/
|
*/
|
||||||
export function joinVoiceChannel(channelId: string): void {
|
export function joinVoiceChannel(channelId: string): void {
|
||||||
const { currentVoiceChannelId, setCurrentVoiceChannel } = useVoiceStore.getState();
|
const { currentVoiceChannelId, setCurrentVoiceChannel, addVoiceUser, removeVoiceUser } = useVoiceStore.getState();
|
||||||
if (currentVoiceChannelId === channelId) return;
|
if (currentVoiceChannelId === channelId) return;
|
||||||
|
|
||||||
|
const myId = useAuthStore.getState().user?.id;
|
||||||
|
|
||||||
// Leave old instance if switching cross-origin
|
// Leave old instance if switching cross-origin
|
||||||
if (currentVoiceChannelId) {
|
if (currentVoiceChannelId) {
|
||||||
const oldOrigin = getChannelOrigin(currentVoiceChannelId);
|
const oldOrigin = getChannelOrigin(currentVoiceChannelId);
|
||||||
@@ -19,8 +22,12 @@ export function joinVoiceChannel(channelId: string): void {
|
|||||||
if (oldOrigin !== newOrigin) {
|
if (oldOrigin !== newOrigin) {
|
||||||
wsSend({ type: 'voice_leave' }, oldOrigin);
|
wsSend({ type: 'voice_leave' }, oldOrigin);
|
||||||
}
|
}
|
||||||
|
// Optimistic: immediately remove self from old channel
|
||||||
|
if (myId) removeVoiceUser(currentVoiceChannelId, myId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentVoiceChannel(channelId);
|
setCurrentVoiceChannel(channelId);
|
||||||
wsSend({ type: 'voice_join', channelId }, getChannelOrigin(channelId));
|
wsSend({ type: 'voice_join', channelId }, getChannelOrigin(channelId));
|
||||||
|
// Optimistic: immediately show self in new channel
|
||||||
|
if (myId) addVoiceUser(channelId, myId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user