feat(call): introduce StageLayout discriminated union

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-05-12 18:43:55 +02:00
parent a9d5e2d430
commit f612c1bb50
+31 -15
View File
@@ -294,12 +294,35 @@ export function InCallPanel({ conversation }: Props) {
? t('app:call.waiting_for_peers', { defaultValue: 'Warte auf andere…' })
: t('app:call.connected');
// Screen shares no longer auto-promote — the user opts in by clicking the
// "Bildschirm anschauen" overlay, which also toggles whether the audio
// plays. Focus falls back to the first tile so focus-mode always has
// something to show when no tile was explicitly picked.
const effectiveFocusedId = focusedId ?? tiles[0]?.id ?? null;
const speaker = tiles.find((p) => p.id === effectiveFocusedId) ?? tiles[0];
// Discord-style precedence:
// 1. focusedId set → 'focus', that tile is the stage.
// 2. ≥2 shares, no pin → 'bento', shares fill the stage, webcams strip.
// 3. exactly 1 share, no pin → 'focus' (auto-promote share).
// 4. no shares, no pin → 'equal-grid'.
type StageLayout =
| { kind: 'equal-grid' }
| { kind: 'focus'; bigTileId: string }
| { kind: 'bento'; shareIds: string[] };
const shareIds = tiles.filter((t) => t.kind === 'screen').map((t) => t.id);
const stageLayout: StageLayout = (() => {
if (focusedId !== null && tiles.some((t) => t.id === focusedId)) {
return { kind: 'focus', bigTileId: focusedId };
}
if (shareIds.length >= 2) return { kind: 'bento', shareIds };
if (shareIds.length === 1 && shareIds[0]) {
return { kind: 'focus', bigTileId: shareIds[0] };
}
return { kind: 'equal-grid' };
})();
// Tile that owns the big stage when layout is 'focus'. Resolved lazily by
// callers below — kept here just so the speaker prop on CallStage/Fullscreen
// stays consistent with the layout decision.
const bigTile =
stageLayout.kind === 'focus'
? tiles.find((t) => t.id === stageLayout.bigTileId)
: undefined;
const controls = (
<CallControls
@@ -369,13 +392,6 @@ export function InCallPanel({ conversation }: Props) {
}));
if (callMode === 'fullscreen') {
// Discord-style: without an explicit pin, fullscreen stays as a stable
// equal-size grid. Focus only kicks in when the user pins a tile
// (focusedId !== null). Auto-promoting the latest speaker caused the
// whole layout to flip on every utterance, which is not what users
// expect from a "cinema" view.
const hasFocus = focusedId !== null;
const effectiveSpeaker = hasFocus ? speaker : undefined;
return (
<>
{micError && (
@@ -391,7 +407,7 @@ export function InCallPanel({ conversation }: Props) {
)}
<FullscreenCall
tiles={tiles}
speaker={effectiveSpeaker}
speaker={bigTile}
remoteScreenShares={remoteScreenShares}
conversationMembers={conversation.members}
activeSpeakers={activeSpeakers}
@@ -540,7 +556,7 @@ export function InCallPanel({ conversation }: Props) {
<CallStage
tiles={tiles}
speaker={speaker}
speaker={bigTile}
mode={callMode}
activeSpeakers={activeSpeakers}
e2ee={isE2EEActive}