From 5e4a5c2ee9306debf15b86808263d55fbf7925dc Mon Sep 17 00:00:00 2001
From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com>
Date: Wed, 4 Mar 2026 20:57:15 +0100
Subject: [PATCH] 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.
---
.../src/components/layout/ChannelSidebar.tsx | 5 ++--
.../web/src/components/layout/MainContent.tsx | 8 +++---
packages/web/src/hooks/useWebSocket.ts | 16 +++++++-----
packages/web/src/utils/voice.ts | 26 +++++++++++++++++++
4 files changed, 41 insertions(+), 14 deletions(-)
create mode 100644 packages/web/src/utils/voice.ts
diff --git a/packages/web/src/components/layout/ChannelSidebar.tsx b/packages/web/src/components/layout/ChannelSidebar.tsx
index 92065502..20d6f41a 100644
--- a/packages/web/src/components/layout/ChannelSidebar.tsx
+++ b/packages/web/src/components/layout/ChannelSidebar.tsx
@@ -15,6 +15,7 @@ import { getActiveRoom } from '../../hooks/useLiveKit';
import { AudioManager } from '../../audio/AudioManager';
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
import { parseFederatedUsername, isSelf } from '../../utils/identity';
+import { joinVoiceChannel } from '../../utils/voice';
export function ChannelSidebar() {
const servers = useServerStore((s) => s.servers);
@@ -29,7 +30,6 @@ export function ChannelSidebar() {
const members = useServerStore((s) => s.members);
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
- const setCurrentVoiceChannel = useVoiceStore((s) => s.setCurrentVoiceChannel);
const isMuted = useVoiceStore((s) => s.isMuted);
const isDeafened = useVoiceStore((s) => s.isDeafened);
const toggleMic = useVoiceStore((s) => s.toggleMic);
@@ -104,8 +104,7 @@ export function ChannelSidebar() {
navigate(`/channels/${currentServerId}/${channelId}`);
return;
}
- setCurrentVoiceChannel(channelId);
- wsSend({ type: 'voice_join', channelId }, getChannelOrigin(channelId));
+ joinVoiceChannel(channelId);
navigate(`/channels/${currentServerId}/${channelId}`);
};
diff --git a/packages/web/src/components/layout/MainContent.tsx b/packages/web/src/components/layout/MainContent.tsx
index c2df75f4..dd212a87 100644
--- a/packages/web/src/components/layout/MainContent.tsx
+++ b/packages/web/src/components/layout/MainContent.tsx
@@ -1,5 +1,5 @@
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 { useUIStore } from '../../stores/uiStore';
import { useAuthStore } from '../../stores/authStore';
@@ -15,6 +15,7 @@ import { useVoiceStore } from '../../stores/voiceStore';
import { wsSend } from '../../hooks/useWebSocket';
import { MemberListToggleButton } from './MemberListToggleButton';
import { isSelf } from '../../utils/identity';
+import { joinVoiceChannel } from '../../utils/voice';
export function MainContent() {
// 1. ALL HOOKS AT THE TOP
@@ -258,10 +259,7 @@ export function MainContent() {
No one is currently in this voice channel.