From 89b76eff2353e5af9fb6a58ca932ad0f1b0e2bde Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 11 May 2026 22:37:13 +0000 Subject: [PATCH] use spotlight view for 1:1s when showControls=false and hide more buttons --- src/room/InCallView.tsx | 34 ++++-- src/room/ReactionsOverlay.module.css | 5 + src/state/CallViewModel/CallViewModel.ts | 136 +++++++++++++---------- src/tile/SpotlightTile.tsx | 4 +- 4 files changed, 113 insertions(+), 66 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 7fc97e27..7f122f06 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -67,6 +67,7 @@ import { } from "../reactions/useReactionsSender"; import { ReactionsAudioRenderer } from "./ReactionAudioRenderer"; import { ReactionsOverlay } from "./ReactionsOverlay"; +import { CallClock } from "./CallClock"; import { CallEventAudioRenderer } from "./CallEventAudioRenderer"; import { debugTileLayout as debugTileLayoutSetting, @@ -364,13 +365,17 @@ export const InCallView: FC = ({ } case HeaderStyle.None: // Cosmetic header to fill out space while still affecting the bounds of - // the grid - header = ( -
- ); + // the grid. In kiosk mode (showControls=false) we drop it entirely so + // the layout fills the window edge-to-edge — mirrors the existing + // suppression of the footer in this combo. + if (showControls) { + header = ( +
+ ); + } break; case HeaderStyle.Standard: header = ( @@ -433,6 +438,12 @@ export const InCallView: FC = ({ // need to remove them from the accessibility tree and block focus. const contentObscured = reconnecting || earpieceMode; + // In kiosk mode (showControls=false), suppress the per-tile fullscreen and + // zoom (expand/collapse) buttons when we're already rendering a single tile + // edge-to-edge in spotlight-expanded layout — they'd just be visual noise. + const suppressSpotlightTileButtons = + !showControls && layout.type === "spotlight-expanded"; + const Tile = useMemo( () => function Tile({ @@ -469,7 +480,10 @@ export const InCallView: FC = ({ ref={ref} vm={model} expanded={spotlightExpanded} - onToggleExpanded={onToggleExpanded} + onToggleExpanded={ + suppressSpotlightTileButtons ? null : onToggleExpanded + } + hideFullscreen={suppressSpotlightTileButtons} targetWidth={targetWidth} targetHeight={targetHeight} showIndicators={showSpotlightIndicatorsValue} @@ -479,7 +493,7 @@ export const InCallView: FC = ({ /> ); }, - [vm, openProfile, contentObscured], + [vm, openProfile, contentObscured, suppressSpotlightTileButtons], ); const layouts = useMemo(() => { @@ -628,6 +642,8 @@ export const InCallView: FC = ({ {reconnectingToast} {earpieceOverlay} + {/* Clock hidden for now (retained for re-enabling later) */} + {false && } {footer} {layout.type !== "pip" && ( <> diff --git a/src/room/ReactionsOverlay.module.css b/src/room/ReactionsOverlay.module.css index 3738dc09..213eb86b 100644 --- a/src/room/ReactionsOverlay.module.css +++ b/src/room/ReactionsOverlay.module.css @@ -11,6 +11,11 @@ .reaction { font-size: 32pt; + /* WPEWebKit on Linux has no system colour-emoji font and its emoji-font + matching heuristic does not always honour an `@font-face` registration + buried late in the family stack. Putting "Twemoji" first guarantees the + bundled COLR font is picked for the emoji codepoint. */ + font-family: "Twemoji", var(--cpd-font-family-sans); /* Reactions are "active" for 3 seconds (as per REACTION_ACTIVE_TIME_MS), give a bit more time for it to fade out. */ animation-duration: 4s; animation-name: reaction-up; diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 4c8602d7..7cbf7af4 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -1176,66 +1176,90 @@ export function createCallViewModel$( map((spotlight) => ({ type: "pip", spotlight })), ); + const layoutMediaByWindowMode$: Observable = windowMode$.pipe( + switchMap((windowMode) => { + switch (windowMode) { + case "normal": + return gridMode$.pipe( + switchMap((gridMode) => { + switch (gridMode) { + case "grid": + return oneOnOneLayoutMedia$.pipe( + switchMap((oneOnOne) => + oneOnOne === null ? gridLayoutMedia$ : of(oneOnOne), + ), + ); + case "spotlight": + return spotlightExpanded$.pipe( + switchMap((expanded) => + expanded + ? spotlightExpandedLayoutMedia$ + : spotlightLandscapeLayoutMedia$, + ), + ); + } + }), + ); + case "narrow": + return oneOnOneLayoutMedia$.pipe( + switchMap((oneOnOne) => + oneOnOne === null + ? combineLatest([grid$, spotlight$], (grid, spotlight) => + grid.length > smallMobileCallThreshold || + spotlight.some((vm) => vm.type === "screen share") + ? spotlightPortraitLayoutMedia$ + : gridLayoutMedia$, + ).pipe(switchAll()) + : // The expanded spotlight layout makes for a better one-on-one + // experience in narrow windows + spotlightExpandedLayoutMedia$, + ), + ); + case "flat": + return gridMode$.pipe( + switchMap((gridMode) => { + switch (gridMode) { + case "grid": + // Yes, grid mode actually gets you a "spotlight" layout in + // this window mode. + return spotlightLandscapeLayoutMedia$; + case "spotlight": + return spotlightExpandedLayoutMedia$; + } + }), + ); + case "pip": + return pipLayoutMedia$; + } + }), + ); + + // In kiosk mode (showControls=false) with a single user-media tile and + // no screen share, render that tile edge-to-edge as a full-window + // spotlight rather than as a floating PiP inside a one-on-one layout. + // This is what we'd produce on a small form-factor 1:1 anyway. + const soloKioskLayoutMedia$: Observable = + getUrlParams().showControls + ? of(null) + : combineLatest([userMedia$, screenShares$]).pipe( + map(([userMedia, screenShares]) => { + if (screenShares.length > 0) return null; + if (userMedia.length !== 1) return null; + return { + type: "spotlight-expanded" as const, + spotlight: [userMedia[0]], + }; + }), + ); + /** * The media to be used to produce a layout. */ const layoutMedia$ = scope.behavior( - windowMode$.pipe( - switchMap((windowMode) => { - switch (windowMode) { - case "normal": - return gridMode$.pipe( - switchMap((gridMode) => { - switch (gridMode) { - case "grid": - return oneOnOneLayoutMedia$.pipe( - switchMap((oneOnOne) => - oneOnOne === null ? gridLayoutMedia$ : of(oneOnOne), - ), - ); - case "spotlight": - return spotlightExpanded$.pipe( - switchMap((expanded) => - expanded - ? spotlightExpandedLayoutMedia$ - : spotlightLandscapeLayoutMedia$, - ), - ); - } - }), - ); - case "narrow": - return oneOnOneLayoutMedia$.pipe( - switchMap((oneOnOne) => - oneOnOne === null - ? combineLatest([grid$, spotlight$], (grid, spotlight) => - grid.length > smallMobileCallThreshold || - spotlight.some((vm) => vm.type === "screen share") - ? spotlightPortraitLayoutMedia$ - : gridLayoutMedia$, - ).pipe(switchAll()) - : // The expanded spotlight layout makes for a better one-on-one - // experience in narrow windows - spotlightExpandedLayoutMedia$, - ), - ); - case "flat": - return gridMode$.pipe( - switchMap((gridMode) => { - switch (gridMode) { - case "grid": - // Yes, grid mode actually gets you a "spotlight" layout in - // this window mode. - return spotlightLandscapeLayoutMedia$; - case "spotlight": - return spotlightExpandedLayoutMedia$; - } - }), - ); - case "pip": - return pipLayoutMedia$; - } - }), + soloKioskLayoutMedia$.pipe( + switchMap((solo) => + solo !== null ? of(solo) : layoutMediaByWindowMode$, + ), ), ); diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 808773b0..ca4be357 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -384,6 +384,7 @@ interface Props { focusable: boolean; className?: string; style?: ComponentProps["style"]; + hideFullscreen?: boolean; } export const SpotlightTile: FC = ({ @@ -397,6 +398,7 @@ export const SpotlightTile: FC = ({ focusable = true, className, style, + hideFullscreen = false, }) => { const { t } = useTranslation(); const [ourRef, root$] = useObservableRef(null); @@ -520,7 +522,7 @@ export const SpotlightTile: FC = ({ {visibleMedia?.type === "screen share" && !visibleMedia.local && ( )} - {platform === "desktop" && ( + {platform === "desktop" && !hideFullscreen && (