MobileVoiceFullScreen and MobileVoiceMiniBar were missing voice_leave
(and dm_call_end) WS messages in their disconnect handlers, leaving
the server showing stale voice state. Now matches VoiceControls pattern.
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.