From 71b383ce7f56dbfe0a246410d5ea1cd70f245ac9 Mon Sep 17 00:00:00 2001
From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com>
Date: Thu, 7 May 2026 23:44:34 +0200
Subject: [PATCH] =?UTF-8?q?fix(mobile):=20voice=20join=20flow=20=E2=80=94?=
=?UTF-8?q?=20correct=20screen=20key=20+=20remove=20desktop=20PiP=20from?=
=?UTF-8?q?=20mobile=20branch?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two compounding root causes produced a "PiP-style grey view" briefly visible after Join Voice on mobile:
1. MobileSpacesScreen.handleVoiceJoin pushed 'voice' onto the mobile stack. MobileShell.screenMap only has 'voice-full' — the renderer returned null for 'voice' while the root screen sat under `visibility: hidden` (stack length > 0). Corrected to 'voice-full' with an explanatory comment.
2. AppLayout mounted the desktop in the mobile branch too. Its visibility check `(isInServerVoice || isInDmCall) && !voiceFullscreen` is true the moment currentVoiceChannelId is set, so the desktop PiP rendered as the only visible UI on top of the hidden mobile shell. Removed from the mobile branch; kept on desktop. Added a comment to prevent regression.
Mobile owns its voice overlay chrome exclusively via MobileVoiceMiniBar + MobileVoiceFullScreen.
Spec: docs/systems/mobile-ui.md gains a "Voice Join Flow (Mobile)" subsection and a leading paragraph in "Voice Overlay" stating that PiP is desktop-only.
---
docs/systems/mobile-ui.md | 13 +++++++++++++
packages/web/src/components/layout/AppLayout.tsx | 8 +++++++-
.../src/components/layout/MobileSpacesScreen.tsx | 8 +++++++-
3 files changed, 27 insertions(+), 2 deletions(-)
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) => {