diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index ca71ff726..5872cfb44 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -74,6 +74,7 @@ import { useAppBarSubtitle, } from "../AppBar.tsx"; import { useBehavior } from "../useBehavior.ts"; +import { constant } from "../state/Behavior.ts"; import { Toast } from "../Toast.tsx"; import overlayStyles from "../Overlay.module.css"; import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx"; @@ -433,6 +434,11 @@ export const InCallView: FC = ({ const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$); const showNameTags = useBehavior(vm.showNameTags$); const showRingingStatus = vm.ringingStatusLocation === "tile"; + const showOutline = useBehavior( + model instanceof GridTileViewModel + ? model.showOutline$ + : constant(false), + ); return model instanceof GridTileViewModel ? ( = ({ showSpeakingIndicators={showSpeakingIndicators} showNameTags={showNameTags} showRingingStatus={showRingingStatus} + showOutline={showOutline} focusable={!contentObscured} /> ) : ( diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 378fe18ae..e39014e4b 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -155,6 +155,7 @@ import { createRingingMedia, type RingingMediaViewModel, } from "../media/RingingMediaViewModel.ts"; +import { type GridTileViewModel } from "../TileViewModel.ts"; const logger = rootLogger.getChild("[CallViewModel]"); //TODO @@ -1482,6 +1483,7 @@ export function createCallViewModel$( ({ tiles: prevTiles }, [media, visibleTiles]) => { let layout: Layout; let newTiles: TileStore; + let pip: GridTileViewModel | undefined; switch (media.type) { case "grid": case "spotlight-landscape": @@ -1507,6 +1509,7 @@ export function createCallViewModel$( landscapePipAlignment$, prevTiles, ); + pip = layout.pip; break; case "one-on-one-portrait": [layout, newTiles] = oneOnOnePortraitLayout( @@ -1515,12 +1518,17 @@ export function createCallViewModel$( portraitPipAlignment$, prevTiles, ); + pip = layout.pip; break; case "pip": [layout, newTiles] = pipLayout(media, prevTiles); break; } + for (const tile of newTiles.gridTiles) { + tile.setShowOutline(tile === pip); + } + return { layout, tiles: newTiles }; }, { layout: null, tiles: TileStore.empty() }, diff --git a/src/state/TileViewModel.ts b/src/state/TileViewModel.ts index eeec0c889..81aac8298 100644 --- a/src/state/TileViewModel.ts +++ b/src/state/TileViewModel.ts @@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ +import { BehaviorSubject } from "rxjs"; + import { type Behavior } from "./Behavior"; import { type MediaViewModel } from "./media/MediaViewModel"; import { type RingingMediaViewModel } from "./media/RingingMediaViewModel"; @@ -17,12 +19,18 @@ function createId(): string { export class GridTileViewModel { public readonly id = createId(); + private readonly _showOutline$ = new BehaviorSubject(false); + public readonly showOutline$: Behavior = this._showOutline$; public constructor( public readonly media$: Behavior< UserMediaViewModel | RingingMediaViewModel >, ) {} + + public setShowOutline(value: boolean): void { + this._showOutline$.next(value); + } } export class SpotlightTileViewModel { diff --git a/src/tile/GridTile.module.css b/src/tile/GridTile.module.css index 971b56906..3ebb9bf75 100644 --- a/src/tile/GridTile.module.css +++ b/src/tile/GridTile.module.css @@ -66,6 +66,11 @@ borders don't support gradients */ opacity: 1; } +.tile.outline { + outline: var(--cpd-border-width-1) solid + var(--cpd-color-border-interactive-secondary); +} + @media (hover: hover) { .tile:hover { outline: var(--cpd-border-width-2) solid diff --git a/src/tile/GridTile.test.tsx b/src/tile/GridTile.test.tsx index a44fcdfe4..3ee528755 100644 --- a/src/tile/GridTile.test.tsx +++ b/src/tile/GridTile.test.tsx @@ -78,6 +78,7 @@ test("GridTile is accessible", async () => { showSpeakingIndicators showNameTags showRingingStatus + showOutline focusable /> , @@ -110,6 +111,7 @@ test("GridTile displays ringing media", async () => { showSpeakingIndicators showNameTags showRingingStatus + showOutline focusable /> , diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index 8e6096fb5..657bf0bc8 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -398,6 +398,7 @@ interface GridTileProps { showSpeakingIndicators: boolean; showNameTags: boolean; showRingingStatus: boolean; + showOutline: boolean; focusable: boolean; } @@ -406,7 +407,9 @@ export const GridTile: FC = ({ vm, showSpeakingIndicators, showRingingStatus, + showOutline, onOpenProfile, + className, ...props }) => { const ourRef = useRef(null); @@ -423,6 +426,7 @@ export const GridTile: FC = ({ displayName={displayName} mxcAvatarUrl={mxcAvatarUrl} showStatus={showRingingStatus} + className={classNames(className, { [styles.outline]: showOutline })} {...props} /> ); @@ -435,6 +439,7 @@ export const GridTile: FC = ({ onOpenProfile={onOpenProfile} displayName={displayName} mxcAvatarUrl={mxcAvatarUrl} + className={classNames(className, { [styles.outline]: showOutline })} {...props} /> ); @@ -446,6 +451,7 @@ export const GridTile: FC = ({ showSpeakingIndicators={showSpeakingIndicators} displayName={displayName} mxcAvatarUrl={mxcAvatarUrl} + className={classNames(className, { [styles.outline]: showOutline })} {...props} /> );