From e56533918e01474bb243b8db3880c2450914fdd9 Mon Sep 17 00:00:00 2001 From: byGalax Date: Tue, 12 May 2026 18:54:23 +0200 Subject: [PATCH] feat(call): multi-share bento layout in stage + fullscreen Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/components/InCallPanel.tsx | 127 +++++++++++++++++++- 1 file changed, 122 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/components/InCallPanel.tsx b/apps/desktop/src/components/InCallPanel.tsx index f240c99..b413f0c 100644 --- a/apps/desktop/src/components/InCallPanel.tsx +++ b/apps/desktop/src/components/InCallPanel.tsx @@ -557,7 +557,7 @@ export function InCallPanel({ conversation }: Props) { ; e2ee: boolean; remoteScreenShares: { @@ -981,7 +986,7 @@ function TileRender({ function CallStage({ tiles, speaker, - mode, + stageLayout, activeSpeakers, e2ee, remoteScreenShares, @@ -990,7 +995,7 @@ function CallStage({ onTileContextMenu, compact = false, }: StageProps) { - if (mode === 'focus' && speaker) { + if (stageLayout.kind === 'focus' && speaker) { const others = tiles.filter((p) => p.id !== speaker.id); return (
@@ -1034,7 +1039,66 @@ function CallStage({ ); } - // Grid + if (stageLayout.kind === 'bento') { + const shares = tiles.filter((t) => stageLayout.shareIds.includes(t.id)); + const webcams = tiles.filter((t) => !stageLayout.shareIds.includes(t.id)); + const bentoCols = gridColsFor(shares.length); + return ( +
+
+
+ {shares.map((s) => ( +
+ onFocusTile(s.id)} + {...(onTileContextMenu + ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(s, e) } + : {})} + /> +
+ ))} +
+
+ {webcams.length > 0 && ( +
+ {webcams.map((w) => ( +
+ onFocusTile(w.id)} + {...(onTileContextMenu + ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(w, e) } + : {})} + /> +
+ ))} +
+ )} +
+ ); + } + + // Grid (equal-grid fallthrough) const gridClass = gridColsFor(tiles.length); return (
@@ -1205,6 +1269,17 @@ function FullscreenCall({ const hasFocus = speaker !== undefined; const others = hasFocus ? tiles.filter((p) => p.id !== speaker!.id) : []; + const fsShareIds = tiles + .filter((t) => t.kind === 'screen') + .map((t) => t.id); + const bentoMode = !hasFocus && fsShareIds.length >= 2; + const bentoShares = bentoMode + ? tiles.filter((t) => fsShareIds.includes(t.id)) + : []; + const bentoWebcams = bentoMode + ? tiles.filter((t) => !fsShareIds.includes(t.id)) + : []; + // Active-speaker reorder + paginate. When more than GRID_PAGE_SIZE tiles // exist, slice them into pages and prioritize active speakers onto page 1. // Otherwise keep a stable order (Discord-style) so tiles don't shuffle @@ -1273,6 +1348,48 @@ function FullscreenCall({
)} + ) : bentoMode ? ( +
+
+
+ {bentoShares.map((s) => ( +
+ onFocusTile(s.id)} + {...(onTileContextMenu + ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(s, e) } + : {})} + /> +
+ ))} +
+
+ {bentoWebcams.length > 0 && ( +
+ {bentoWebcams.map((w) => ( +
+ onFocusTile(w.id)} + {...(onTileContextMenu + ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(w, e) } + : {})} + /> +
+ ))} +
+ )} +
) : (