fix: clean up cross-instance voice state when switching federated servers
Send explicit voice_leave to the old instance when joining voice on a different origin, preventing stale voice state from showing the user in two channels. Also make WS reconnect voice re-registration origin-aware so remote reconnects properly restore voice state.
This commit is contained in:
@@ -15,6 +15,7 @@ import { getActiveRoom } from '../../hooks/useLiveKit';
|
|||||||
import { AudioManager } from '../../audio/AudioManager';
|
import { AudioManager } from '../../audio/AudioManager';
|
||||||
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||||
import { parseFederatedUsername, isSelf } from '../../utils/identity';
|
import { parseFederatedUsername, isSelf } from '../../utils/identity';
|
||||||
|
import { joinVoiceChannel } from '../../utils/voice';
|
||||||
|
|
||||||
export function ChannelSidebar() {
|
export function ChannelSidebar() {
|
||||||
const servers = useServerStore((s) => s.servers);
|
const servers = useServerStore((s) => s.servers);
|
||||||
@@ -29,7 +30,6 @@ export function ChannelSidebar() {
|
|||||||
const members = useServerStore((s) => s.members);
|
const members = useServerStore((s) => s.members);
|
||||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||||
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
|
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
|
||||||
const setCurrentVoiceChannel = useVoiceStore((s) => s.setCurrentVoiceChannel);
|
|
||||||
const isMuted = useVoiceStore((s) => s.isMuted);
|
const isMuted = useVoiceStore((s) => s.isMuted);
|
||||||
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
||||||
const toggleMic = useVoiceStore((s) => s.toggleMic);
|
const toggleMic = useVoiceStore((s) => s.toggleMic);
|
||||||
@@ -104,8 +104,7 @@ export function ChannelSidebar() {
|
|||||||
navigate(`/channels/${currentServerId}/${channelId}`);
|
navigate(`/channels/${currentServerId}/${channelId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCurrentVoiceChannel(channelId);
|
joinVoiceChannel(channelId);
|
||||||
wsSend({ type: 'voice_join', channelId }, getChannelOrigin(channelId));
|
|
||||||
navigate(`/channels/${currentServerId}/${channelId}`);
|
navigate(`/channels/${currentServerId}/${channelId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { useServerStore, getChannelOrigin } from '../../stores/serverStore';
|
import { useServerStore } from '../../stores/serverStore';
|
||||||
import { useChatStore } from '../../stores/chatStore';
|
import { useChatStore } from '../../stores/chatStore';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
@@ -15,6 +15,7 @@ import { useVoiceStore } from '../../stores/voiceStore';
|
|||||||
import { wsSend } from '../../hooks/useWebSocket';
|
import { wsSend } from '../../hooks/useWebSocket';
|
||||||
import { MemberListToggleButton } from './MemberListToggleButton';
|
import { MemberListToggleButton } from './MemberListToggleButton';
|
||||||
import { isSelf } from '../../utils/identity';
|
import { isSelf } from '../../utils/identity';
|
||||||
|
import { joinVoiceChannel } from '../../utils/voice';
|
||||||
|
|
||||||
export function MainContent() {
|
export function MainContent() {
|
||||||
// 1. ALL HOOKS AT THE TOP
|
// 1. ALL HOOKS AT THE TOP
|
||||||
@@ -258,10 +259,7 @@ export function MainContent() {
|
|||||||
<p className="text-txt-tertiary text-[15px]">No one is currently in this voice channel.</p>
|
<p className="text-txt-tertiary text-[15px]">No one is currently in this voice channel.</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => joinVoiceChannel(currentChannelId)}
|
||||||
useVoiceStore.getState().setCurrentVoiceChannel(currentChannelId);
|
|
||||||
wsSend({ type: 'voice_join', channelId: currentChannelId }, getChannelOrigin(currentChannelId));
|
|
||||||
}}
|
|
||||||
className="relative z-10 px-8 py-3 bg-accent-primary hover:bg-accent-primary-hover text-white font-semibold rounded-full transition-all text-[15px] shadow-[0_4px_20px_rgba(124,108,246,0.3)]"
|
className="relative z-10 px-8 py-3 bg-accent-primary hover:bg-accent-primary-hover text-white font-semibold rounded-full transition-all text-[15px] shadow-[0_4px_20px_rgba(124,108,246,0.3)]"
|
||||||
>
|
>
|
||||||
Join Voice
|
Join Voice
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import { useServerStore } from '../stores/serverStore';
|
import { useServerStore, getChannelOrigin } from '../stores/serverStore';
|
||||||
import { useChatStore } from '../stores/chatStore';
|
import { useChatStore } from '../stores/chatStore';
|
||||||
import { useVoiceStore } from '../stores/voiceStore';
|
import { useVoiceStore } from '../stores/voiceStore';
|
||||||
import { useSocialStore } from '../stores/socialStore';
|
import { useSocialStore } from '../stores/socialStore';
|
||||||
@@ -162,13 +162,17 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-register in voice channel if we're still connected to LiveKit (home only)
|
// Re-register in voice channel if we're still connected to LiveKit
|
||||||
if (isHome) {
|
// Origin-aware: re-sends voice_join only if the voice channel belongs to this origin
|
||||||
|
{
|
||||||
const { currentVoiceChannelId, isMuted: curMuted, isDeafened: curDeafened, isCameraOn: curCamera, isScreenSharing: curScreen } = useVoiceStore.getState();
|
const { currentVoiceChannelId, isMuted: curMuted, isDeafened: curDeafened, isCameraOn: curCamera, isScreenSharing: curScreen } = useVoiceStore.getState();
|
||||||
if (currentVoiceChannelId) {
|
if (currentVoiceChannelId) {
|
||||||
console.log('[WebSocket] Re-syncing voice status on reconnect:', { currentVoiceChannelId, curMuted, curDeafened, curCamera, curScreen });
|
const voiceOrigin = getChannelOrigin(currentVoiceChannelId);
|
||||||
wsSend({ type: 'voice_join', channelId: currentVoiceChannelId });
|
if (voiceOrigin === origin) {
|
||||||
wsSend({ type: 'voice_status', isMuted: curMuted, isDeafened: curDeafened, isCameraOn: curCamera, isScreenSharing: 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_status', isMuted: curMuted, isDeafened: curDeafened, isCameraOn: curCamera, isScreenSharing: curScreen }, origin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useVoiceStore } from '../stores/voiceStore';
|
||||||
|
import { getChannelOrigin } from '../stores/serverStore';
|
||||||
|
import { wsSend } from '../hooks/useWebSocket';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized voice channel join that handles cross-instance cleanup.
|
||||||
|
* When switching from a channel on Instance A to one on Instance B,
|
||||||
|
* this sends an explicit voice_leave to Instance A first so it
|
||||||
|
* broadcasts a leave event and the client cleans up stale voice state.
|
||||||
|
*/
|
||||||
|
export function joinVoiceChannel(channelId: string): void {
|
||||||
|
const { currentVoiceChannelId, setCurrentVoiceChannel } = useVoiceStore.getState();
|
||||||
|
if (currentVoiceChannelId === channelId) return;
|
||||||
|
|
||||||
|
// Leave old instance if switching cross-origin
|
||||||
|
if (currentVoiceChannelId) {
|
||||||
|
const oldOrigin = getChannelOrigin(currentVoiceChannelId);
|
||||||
|
const newOrigin = getChannelOrigin(channelId);
|
||||||
|
if (oldOrigin !== newOrigin) {
|
||||||
|
wsSend({ type: 'voice_leave' }, oldOrigin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentVoiceChannel(channelId);
|
||||||
|
wsSend({ type: 'voice_join', channelId }, getChannelOrigin(channelId));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user