diff --git a/docs/systems/mobile-ui.md b/docs/systems/mobile-ui.md index 869447cd..4cf50cd7 100644 --- a/docs/systems/mobile-ui.md +++ b/docs/systems/mobile-ui.md @@ -453,6 +453,19 @@ interface MobileScreenHeaderProps { ## Voice Overlay +**Mobile is the sole owner of voice overlay chrome.** `` is a desktop-only component; `AppLayout`'s mobile branch does NOT mount it. The mobile equivalents are `MobileVoiceMiniBar` (always-visible call-active overlay between the screen stack and bottom nav) and `MobileVoiceFullScreen` (pushed-screen full takeover). Mounting PiP on mobile would render its 320×180 floating box on top of the mobile shell whenever `currentVoiceChannelId` was set but `voice-full` was not on the stack — the symptom that triggered this split was a "PiP-style grey view" appearing immediately after `MobileVoiceJoinSheet`'s Join button (compounded by a misnamed screen key — see "Voice Join Flow" below). + +### Voice Join Flow (Mobile) + +`MobileSpacesScreen.handleVoiceJoin` is the single entry point on mobile: + +1. Apply pre-mute if requested (`voiceStore.setMuted(true)`). +2. Call `joinVoiceChannel(channelId, connectFn)` (which sends `voice_join` WS, gets a LiveKit token, and connects the room). +3. Close the join sheet (`setVoiceJoinChannelId(null)`). +4. `pushMobileScreen('voice-full')` — must match the canonical key in `MobileShell.screenMap`. A historical bug passed `'voice'`, which had no entry; the renderer returned null while the root screen sat under `visibility: hidden`, making the (incorrectly mounted) desktop PiP the only visible UI. Both have been root-fixed. + +The user lands directly in `MobileVoiceFullScreen` — there is no intermediate "connecting" screen. While LiveKit handshakes, the participant grid renders with whatever the WS `voice_users` map already contains (typically just the local user) and updates as remote participants arrive. + ### MobileVoiceMiniBar File: `MobileVoiceMiniBar.tsx` diff --git a/packages/web/src/components/layout/AppLayout.tsx b/packages/web/src/components/layout/AppLayout.tsx index 55cc302b..8976658e 100644 --- a/packages/web/src/components/layout/AppLayout.tsx +++ b/packages/web/src/components/layout/AppLayout.tsx @@ -406,7 +406,13 @@ export function AppLayout() { - + {/* PictureInPicture is desktop-only. Mobile has its own purpose-built + voice chrome (MobileVoiceMiniBar + MobileVoiceFullScreen) mounted + inside MobileShell. Mounting PiP here too caused a "PiP-style grey + view" to appear on top of the mobile shell whenever a voice call + was connected but the voice-full screen was not on the stack + (e.g. immediately after Join, or after popping voice-full back to + the root). */} diff --git a/packages/web/src/components/layout/MobileSpacesScreen.tsx b/packages/web/src/components/layout/MobileSpacesScreen.tsx index 556a5c59..f243f8bb 100644 --- a/packages/web/src/components/layout/MobileSpacesScreen.tsx +++ b/packages/web/src/components/layout/MobileSpacesScreen.tsx @@ -224,7 +224,13 @@ export function MobileSpacesScreen() { const connectFn = useVoiceStore.getState().connectFn; joinVoiceChannel(chId, connectFn ?? undefined); setVoiceJoinChannelId(null); - pushMobileScreen('voice'); + // Push the canonical mobile voice screen key. Previously this passed + // 'voice', which has no entry in MobileShell.screenMap — the renderer + // returned null while the root screen sat under `visibility: hidden`, + // exposing the desktop PictureInPicture popup as the only visible UI + // (the "PiP-style grey view" bug). The screen key must match + // MobileShell.tsx:91 ('voice-full'). + pushMobileScreen('voice-full'); }, [pushMobileScreen]); const toggleCategory = (categoryId: string) => {