Add PiP border in 1on1 layouts

Fixes: #4008
This commit is contained in:
Johannes Marbach
2026-06-19 14:49:59 +02:00
parent 5a68010022
commit 93fae5dd67
6 changed files with 35 additions and 0 deletions

View File

@@ -74,6 +74,7 @@ import {
useAppBarSubtitle, useAppBarSubtitle,
} from "../AppBar.tsx"; } from "../AppBar.tsx";
import { useBehavior } from "../useBehavior.ts"; import { useBehavior } from "../useBehavior.ts";
import { constant } from "../state/Behavior.ts";
import { Toast } from "../Toast.tsx"; import { Toast } from "../Toast.tsx";
import overlayStyles from "../Overlay.module.css"; import overlayStyles from "../Overlay.module.css";
import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx"; import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx";
@@ -94,6 +95,7 @@ declare module "react" {
} }
const logger = rootLogger.getChild("[InCallView]"); const logger = rootLogger.getChild("[InCallView]");
const noOutline$ = constant(false);
export interface ActiveCallProps extends Omit< export interface ActiveCallProps extends Omit<
InCallViewProps, InCallViewProps,
@@ -433,6 +435,9 @@ export const InCallView: FC<InCallViewProps> = ({
const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$); const showSpeakingIndicators = useBehavior(vm.showSpeakingIndicators$);
const showNameTags = useBehavior(vm.showNameTags$); const showNameTags = useBehavior(vm.showNameTags$);
const showRingingStatus = vm.ringingStatusLocation === "tile"; const showRingingStatus = vm.ringingStatusLocation === "tile";
const showOutline = useBehavior(
model instanceof GridTileViewModel ? model.showOutline$ : noOutline$,
);
return model instanceof GridTileViewModel ? ( return model instanceof GridTileViewModel ? (
<GridTile <GridTile
@@ -446,6 +451,7 @@ export const InCallView: FC<InCallViewProps> = ({
showSpeakingIndicators={showSpeakingIndicators} showSpeakingIndicators={showSpeakingIndicators}
showNameTags={showNameTags} showNameTags={showNameTags}
showRingingStatus={showRingingStatus} showRingingStatus={showRingingStatus}
showOutline={showOutline}
focusable={!contentObscured} focusable={!contentObscured}
/> />
) : ( ) : (

View File

@@ -155,6 +155,7 @@ import {
createRingingMedia, createRingingMedia,
type RingingMediaViewModel, type RingingMediaViewModel,
} from "../media/RingingMediaViewModel.ts"; } from "../media/RingingMediaViewModel.ts";
import { type GridTileViewModel } from "../TileViewModel.ts";
const logger = rootLogger.getChild("[CallViewModel]"); const logger = rootLogger.getChild("[CallViewModel]");
//TODO //TODO
@@ -1482,6 +1483,7 @@ export function createCallViewModel$(
({ tiles: prevTiles }, [media, visibleTiles]) => { ({ tiles: prevTiles }, [media, visibleTiles]) => {
let layout: Layout; let layout: Layout;
let newTiles: TileStore; let newTiles: TileStore;
let pip: GridTileViewModel | undefined;
switch (media.type) { switch (media.type) {
case "grid": case "grid":
case "spotlight-landscape": case "spotlight-landscape":
@@ -1507,6 +1509,7 @@ export function createCallViewModel$(
landscapePipAlignment$, landscapePipAlignment$,
prevTiles, prevTiles,
); );
pip = layout.pip;
break; break;
case "one-on-one-portrait": case "one-on-one-portrait":
[layout, newTiles] = oneOnOnePortraitLayout( [layout, newTiles] = oneOnOnePortraitLayout(
@@ -1515,12 +1518,17 @@ export function createCallViewModel$(
portraitPipAlignment$, portraitPipAlignment$,
prevTiles, prevTiles,
); );
pip = layout.pip;
break; break;
case "pip": case "pip":
[layout, newTiles] = pipLayout(media, prevTiles); [layout, newTiles] = pipLayout(media, prevTiles);
break; break;
} }
for (const tile of newTiles.gridTiles) {
tile.setShowOutline(tile === pip);
}
return { layout, tiles: newTiles }; return { layout, tiles: newTiles };
}, },
{ layout: null, tiles: TileStore.empty() }, { layout: null, tiles: TileStore.empty() },

View File

@@ -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. Please see LICENSE in the repository root for full details.
*/ */
import { BehaviorSubject } from "rxjs";
import { type Behavior } from "./Behavior"; import { type Behavior } from "./Behavior";
import { type MediaViewModel } from "./media/MediaViewModel"; import { type MediaViewModel } from "./media/MediaViewModel";
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel"; import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
@@ -17,12 +19,18 @@ function createId(): string {
export class GridTileViewModel { export class GridTileViewModel {
public readonly id = createId(); public readonly id = createId();
private readonly _showOutline$ = new BehaviorSubject(false);
public readonly showOutline$: Behavior<boolean> = this._showOutline$;
public constructor( public constructor(
public readonly media$: Behavior< public readonly media$: Behavior<
UserMediaViewModel | RingingMediaViewModel UserMediaViewModel | RingingMediaViewModel
>, >,
) {} ) {}
public setShowOutline(value: boolean): void {
this._showOutline$.next(value);
}
} }
export class SpotlightTileViewModel { export class SpotlightTileViewModel {

View File

@@ -66,6 +66,11 @@ borders don't support gradients */
opacity: 1; opacity: 1;
} }
.tile.outline {
outline: var(--cpd-border-width-1) solid
var(--cpd-color-border-interactive-secondary);
}
@media (hover: hover) { @media (hover: hover) {
.tile:hover { .tile:hover {
outline: var(--cpd-border-width-2) solid outline: var(--cpd-border-width-2) solid

View File

@@ -78,6 +78,7 @@ test("GridTile is accessible", async () => {
showSpeakingIndicators showSpeakingIndicators
showNameTags showNameTags
showRingingStatus showRingingStatus
showOutline
focusable focusable
/> />
</ReactionsSenderProvider>, </ReactionsSenderProvider>,
@@ -110,6 +111,7 @@ test("GridTile displays ringing media", async () => {
showSpeakingIndicators showSpeakingIndicators
showNameTags showNameTags
showRingingStatus showRingingStatus
showOutline
focusable focusable
/> />
</ReactionsSenderProvider>, </ReactionsSenderProvider>,

View File

@@ -398,6 +398,7 @@ interface GridTileProps {
showSpeakingIndicators: boolean; showSpeakingIndicators: boolean;
showNameTags: boolean; showNameTags: boolean;
showRingingStatus: boolean; showRingingStatus: boolean;
showOutline: boolean;
focusable: boolean; focusable: boolean;
} }
@@ -406,7 +407,9 @@ export const GridTile: FC<GridTileProps> = ({
vm, vm,
showSpeakingIndicators, showSpeakingIndicators,
showRingingStatus, showRingingStatus,
showOutline,
onOpenProfile, onOpenProfile,
className,
...props ...props
}) => { }) => {
const ourRef = useRef<HTMLDivElement | null>(null); const ourRef = useRef<HTMLDivElement | null>(null);
@@ -423,6 +426,7 @@ export const GridTile: FC<GridTileProps> = ({
displayName={displayName} displayName={displayName}
mxcAvatarUrl={mxcAvatarUrl} mxcAvatarUrl={mxcAvatarUrl}
showStatus={showRingingStatus} showStatus={showRingingStatus}
className={classNames(className, { [styles.outline]: showOutline })}
{...props} {...props}
/> />
); );
@@ -435,6 +439,7 @@ export const GridTile: FC<GridTileProps> = ({
onOpenProfile={onOpenProfile} onOpenProfile={onOpenProfile}
displayName={displayName} displayName={displayName}
mxcAvatarUrl={mxcAvatarUrl} mxcAvatarUrl={mxcAvatarUrl}
className={classNames(className, { [styles.outline]: showOutline })}
{...props} {...props}
/> />
); );
@@ -446,6 +451,7 @@ export const GridTile: FC<GridTileProps> = ({
showSpeakingIndicators={showSpeakingIndicators} showSpeakingIndicators={showSpeakingIndicators}
displayName={displayName} displayName={displayName}
mxcAvatarUrl={mxcAvatarUrl} mxcAvatarUrl={mxcAvatarUrl}
className={classNames(className, { [styles.outline]: showOutline })}
{...props} {...props}
/> />
); );