feat: gesture-aware voice connection, remove AppLayout auto-connect

Replace the useEffect-based auto-connect pattern in AppLayout with
direct connect/disconnect calls from user gesture contexts. This is
required for iOS (AudioContext.resume + getUserMedia must happen in
a gesture handler) and aligns with tightening autoplay policies on
desktop browsers.

Architecture:
- voiceStore gains connectFn/disconnectFn refs, registered by AppLayout
  from the single useLiveKit() instance.
- All voice join paths (ChannelSidebar, MobileSpacesScreen, MainContent,
  voice_moved WS handler) pass connectFn to joinVoiceChannel().
- All disconnect paths (VoiceControls, voiceActions, MobileVoiceFullScreen,
  MobileVoiceMiniBar, dm_call_ended/rejected WS handlers, ready handler)
  call disconnectFn() directly.
- dm_call_accepted WS handler calls connectFn() to initiate the DM call
  LiveKit connection.
- The 55-line auto-connect useEffect and lastAttemptedRef are removed.
This commit is contained in:
Jannis Braun
2026-03-23 21:06:19 +01:00
parent 8aa66816d8
commit 241451ca18
11 changed files with 80 additions and 81 deletions
+11
View File
@@ -116,6 +116,11 @@ interface VoiceState {
clearVoiceUsersForOrigin: (origin: string) => void;
leaveVoice: () => void;
handleForceDisconnect: () => void;
// Gesture-aware connect/disconnect refs — registered by AppLayout from useLiveKit()
connectFn: ((channelId: string, isDm?: boolean) => Promise<void>) | null;
disconnectFn: (() => Promise<void>) | null;
setConnectFn: (fn: ((channelId: string, isDm?: boolean) => Promise<void>) | null) => void;
setDisconnectFn: (fn: (() => Promise<void>) | null) => void;
reset: () => void;
}
@@ -471,6 +476,12 @@ export const useVoiceStore = create<VoiceState>()(
});
},
// Gesture-aware connect/disconnect refs — set by AppLayout, read by click handlers
connectFn: null,
disconnectFn: null,
setConnectFn: (fn) => set({ connectFn: fn }),
setDisconnectFn: (fn) => set({ disconnectFn: fn }),
// 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.