feat(call): multi-share bento layout in stage + fullscreen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -557,7 +557,7 @@ export function InCallPanel({ conversation }: Props) {
|
||||
<CallStage
|
||||
tiles={tiles}
|
||||
speaker={bigTile}
|
||||
mode={stageLayout.kind === 'equal-grid' ? 'grid' : 'focus'}
|
||||
stageLayout={stageLayout}
|
||||
activeSpeakers={activeSpeakers}
|
||||
e2ee={isE2EEActive}
|
||||
remoteScreenShares={remoteScreenShares}
|
||||
@@ -891,7 +891,12 @@ function ModeButton({
|
||||
interface StageProps {
|
||||
tiles: Tile[];
|
||||
speaker: Tile | undefined;
|
||||
mode: CallMode;
|
||||
/** Discriminated layout decision driven by InCallPanel's StageLayout
|
||||
* selector. Drives the bento-vs-grid-vs-focus render branch. */
|
||||
stageLayout:
|
||||
| { kind: 'equal-grid' }
|
||||
| { kind: 'focus'; bigTileId: string }
|
||||
| { kind: 'bento'; shareIds: string[] };
|
||||
activeSpeakers: Set<string>;
|
||||
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 (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2.5 p-3">
|
||||
@@ -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 (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2.5 p-3">
|
||||
<div className="min-h-0 flex-1">
|
||||
<div
|
||||
className={
|
||||
'grid h-full max-h-full gap-2 place-content-center ' + bentoCols
|
||||
}
|
||||
>
|
||||
{shares.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className="aspect-video min-h-0 w-full [&>div]:h-full [&>div]:w-full"
|
||||
>
|
||||
<TileRender
|
||||
tile={s}
|
||||
activeSpeakers={activeSpeakers}
|
||||
e2ee={e2ee}
|
||||
remoteScreenShares={remoteScreenShares}
|
||||
conversationMembers={conversationMembers}
|
||||
onClick={() => onFocusTile(s.id)}
|
||||
{...(onTileContextMenu
|
||||
? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(s, e) }
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{webcams.length > 0 && (
|
||||
<div className="flex h-[180px] gap-2.5 overflow-x-auto">
|
||||
{webcams.map((w) => (
|
||||
<div
|
||||
key={w.id}
|
||||
className="aspect-video h-full shrink-0 [&>div]:h-full [&>div]:w-full"
|
||||
>
|
||||
<TileRender
|
||||
tile={w}
|
||||
activeSpeakers={activeSpeakers}
|
||||
e2ee={e2ee}
|
||||
remoteScreenShares={remoteScreenShares}
|
||||
conversationMembers={conversationMembers}
|
||||
size="small"
|
||||
onClick={() => onFocusTile(w.id)}
|
||||
{...(onTileContextMenu
|
||||
? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(w, e) }
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Grid (equal-grid fallthrough)
|
||||
const gridClass = gridColsFor(tiles.length);
|
||||
return (
|
||||
<div className={'min-h-0 flex-1 p-3 ' + (compact ? '' : 'p-4')}>
|
||||
@@ -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({
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : bentoMode ? (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2 p-4">
|
||||
<div className="min-h-0 flex-1">
|
||||
<div className={'grid h-full max-h-full gap-2 place-content-center ' + gridColsFor(bentoShares.length)}>
|
||||
{bentoShares.map((s) => (
|
||||
<div key={s.id} className="aspect-video min-h-0 w-full [&>div]:h-full [&>div]:w-full">
|
||||
<TileRender
|
||||
tile={s}
|
||||
activeSpeakers={activeSpeakers}
|
||||
e2ee={e2ee}
|
||||
remoteScreenShares={remoteScreenShares}
|
||||
conversationMembers={conversationMembers}
|
||||
onClick={() => onFocusTile(s.id)}
|
||||
{...(onTileContextMenu
|
||||
? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(s, e) }
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{bentoWebcams.length > 0 && (
|
||||
<div className="flex h-[180px] gap-2 overflow-x-auto">
|
||||
{bentoWebcams.map((w) => (
|
||||
<div key={w.id} className="aspect-video h-full shrink-0 [&>div]:h-full [&>div]:w-full">
|
||||
<TileRender
|
||||
tile={w}
|
||||
activeSpeakers={activeSpeakers}
|
||||
e2ee={e2ee}
|
||||
remoteScreenShares={remoteScreenShares}
|
||||
conversationMembers={conversationMembers}
|
||||
size="small"
|
||||
onClick={() => onFocusTile(w.id)}
|
||||
{...(onTileContextMenu
|
||||
? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(w, e) }
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-h-0 flex-1 p-4">
|
||||
<div className={'grid h-full gap-2 ' + gridClass}>
|
||||
|
||||
Reference in New Issue
Block a user