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
+19 -2
View File
@@ -65,8 +65,17 @@ export function broadcastDeafenViaLiveKit(): void {
* 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.
*
* @param channelId The channel to join.
* @param connectFn The LiveKit connect function, obtained from
* `useVoiceStore.getState().connectFn`. When provided the
* LiveKit connection is initiated directly within the
* caller's gesture context (required on iOS).
*/
export function joinVoiceChannel(channelId: string): void {
export function joinVoiceChannel(
channelId: string,
connectFn?: (channelId: string, isDm?: boolean) => Promise<void>,
): void {
const { currentVoiceChannelId, setCurrentVoiceChannel, addVoiceUser, removeVoiceUser } = useVoiceStore.getState();
if (currentVoiceChannelId === channelId) return;
@@ -83,8 +92,16 @@ export function joinVoiceChannel(channelId: string): void {
}
setCurrentVoiceChannel(channelId);
// voice_join is now sent by useLiveKit after successful LiveKit connection
// Optimistic: immediately show self in new channel (using origin-aware ID)
const myNewId = getMyUserIdForOrigin(getChannelOrigin(channelId));
if (myNewId) addVoiceUser(channelId, myNewId);
// Direct connection within gesture context
if (connectFn) {
connectFn(channelId).catch((err) => {
console.error('[voice] Connection failed:', err);
setCurrentVoiceChannel(null);
if (myNewId) removeVoiceUser(channelId, myNewId);
});
}
}