Let page background shine through spotlight tiles

By giving spotlight tiles a transparent background in certain layouts.
This commit is contained in:
Robin
2026-06-25 18:22:43 +02:00
parent 360a4ff026
commit 8fa3f33f37
9 changed files with 71 additions and 11 deletions

View File

@@ -26,7 +26,7 @@ export function oneOnOnePortraitLayout(
prevTiles: TileStore, prevTiles: TileStore,
): [OneOnOnePortraitLayout, TileStore] { ): [OneOnOnePortraitLayout, TileStore] {
const update = prevTiles.from(media.pip === undefined ? 0 : 1); const update = prevTiles.from(media.pip === undefined ? 0 : 1);
update.registerSpotlight([media.spotlight], true); update.registerSpotlight([media.spotlight], true, "transparent");
if (media.pip !== undefined) update.registerGridTile(media.pip); if (media.pip !== undefined) update.registerGridTile(media.pip);
const tiles = update.build(); const tiles = update.build();

View File

@@ -21,6 +21,7 @@ export function pipLayout(
update.registerSpotlight( update.registerSpotlight(
media.spotlight, media.spotlight,
platform === "desktop" ? false : true, platform === "desktop" ? false : true,
"transparent",
); );
const tiles = update.build(); const tiles = update.build();
return [ return [

View File

@@ -23,7 +23,7 @@ export function spotlightExpandedLayout(
prevTiles: TileStore, prevTiles: TileStore,
): [SpotlightExpandedLayout, TileStore] { ): [SpotlightExpandedLayout, TileStore] {
const update = prevTiles.from(1); const update = prevTiles.from(1);
update.registerSpotlight(media.spotlight, true); update.registerSpotlight(media.spotlight, true, "transparent");
if (media.pip !== undefined) update.registerPipTile(media.pip); if (media.pip !== undefined) update.registerPipTile(media.pip);
const tiles = update.build(); const tiles = update.build();

View File

@@ -39,12 +39,29 @@ class SpotlightTileData {
this.maximised$.next(value); this.maximised$.next(value);
} }
private readonly bgStyle$: BehaviorSubject<"solid" | "transparent">;
public get bgStyle(): "solid" | "transparent" {
return this.bgStyle$.value;
}
public set bgStyle(value: "solid" | "transparent") {
this.bgStyle$.next(value);
}
public readonly vm: SpotlightTileViewModel; public readonly vm: SpotlightTileViewModel;
public constructor(media: MediaViewModel[], maximised: boolean) { public constructor(
media: MediaViewModel[],
maximised: boolean,
bgStyle: "solid" | "transparent",
) {
this.media$ = new BehaviorSubject(media); this.media$ = new BehaviorSubject(media);
this.maximised$ = new BehaviorSubject(maximised); this.maximised$ = new BehaviorSubject(maximised);
this.vm = new SpotlightTileViewModel(this.media$, this.maximised$); this.bgStyle$ = new BehaviorSubject(bgStyle);
this.vm = new SpotlightTileViewModel(
this.media$,
this.maximised$,
this.bgStyle$,
);
} }
} }
@@ -157,7 +174,11 @@ export class TileStoreBuilder {
* Sets the contents of the spotlight tile. If this is never called, there * Sets the contents of the spotlight tile. If this is never called, there
* will be no spotlight tile. * will be no spotlight tile.
*/ */
public registerSpotlight(media: MediaViewModel[], maximised: boolean): void { public registerSpotlight(
media: MediaViewModel[],
maximised: boolean,
bgStyle: "solid" | "transparent" = "solid",
): void {
if (DEBUG_ENABLED) if (DEBUG_ENABLED)
logger.debug( logger.debug(
`[TileStore, ${this.generation}] register spotlight: ${media.map((m) => m.displayName$.value)}`, `[TileStore, ${this.generation}] register spotlight: ${media.map((m) => m.displayName$.value)}`,
@@ -169,11 +190,12 @@ export class TileStoreBuilder {
// Reuse the previous spotlight tile if it exists // Reuse the previous spotlight tile if it exists
if (this.prevSpotlight === null) { if (this.prevSpotlight === null) {
this.spotlight = new SpotlightTileData(media, maximised); this.spotlight = new SpotlightTileData(media, maximised, bgStyle);
} else { } else {
this.spotlight = this.prevSpotlight; this.spotlight = this.prevSpotlight;
this.spotlight.media = media; this.spotlight.media = media;
this.spotlight.maximised = maximised; this.spotlight.maximised = maximised;
this.spotlight.bgStyle = bgStyle;
} }
} }

View File

@@ -37,6 +37,7 @@ export class SpotlightTileViewModel {
public constructor( public constructor(
public readonly media$: Behavior<MediaViewModel[]>, public readonly media$: Behavior<MediaViewModel[]>,
public readonly maximised$: Behavior<boolean>, public readonly maximised$: Behavior<boolean>,
public readonly bgStyle$: Behavior<"solid" | "transparent">,
) {} ) {}
} }

View File

@@ -41,13 +41,16 @@ Please see LICENSE in the repository root for full details.
.bg { .bg {
grid-area: content; grid-area: content;
background-color: var(--video-tile-background);
inline-size: 100%; inline-size: 100%;
block-size: 100%; block-size: 100%;
border-radius: inherit; border-radius: inherit;
contain: strict; contain: strict;
} }
.media[data-bg-style="solid"] .bg {
background-color: var(--video-tile-background);
}
.avatar { .avatar {
position: absolute; position: absolute;
top: 50%; top: 50%;

View File

@@ -43,6 +43,7 @@ interface Props extends ComponentProps<typeof animated.div> {
displayName: string; displayName: string;
mxcAvatarUrl: string | undefined; mxcAvatarUrl: string | undefined;
avatarStyle?: "solid" | "translucent"; avatarStyle?: "solid" | "translucent";
bgStyle?: "solid" | "transparent";
focusable: boolean; focusable: boolean;
primaryButton?: ReactNode; primaryButton?: ReactNode;
raisedHandTime?: Date; raisedHandTime?: Date;
@@ -73,6 +74,7 @@ export const MediaView: FC<Props> = ({
displayName, displayName,
mxcAvatarUrl, mxcAvatarUrl,
avatarStyle = "solid", avatarStyle = "solid",
bgStyle = "solid",
focusable, focusable,
primaryButton, primaryButton,
status, status,
@@ -118,6 +120,7 @@ export const MediaView: FC<Props> = ({
ref={ref} ref={ref}
data-testid="videoTile" data-testid="videoTile"
data-video-fit={videoFit} data-video-fit={videoFit}
data-bg-style={bgStyle}
{...props} {...props}
> >
<div className={styles.bg}> <div className={styles.bg}>

View File

@@ -58,7 +58,13 @@ test("SpotlightTile is accessible", async () => {
const toggleExpanded = vi.fn(); const toggleExpanded = vi.fn();
const { container } = render( const { container } = render(
<SpotlightTile <SpotlightTile
vm={new SpotlightTileViewModel(constant([vm1, vm2]), constant(false))} vm={
new SpotlightTileViewModel(
constant([vm1, vm2]),
constant(false),
constant("solid"),
)
}
targetWidth={300} targetWidth={300}
targetHeight={200} targetHeight={200}
expanded={false} expanded={false}
@@ -101,7 +107,13 @@ test("Screen share volume UI is shown when screen share has audio", async () =>
const { container } = render( const { container } = render(
<TooltipProvider> <TooltipProvider>
<SpotlightTile <SpotlightTile
vm={new SpotlightTileViewModel(constant([vm]), constant(false))} vm={
new SpotlightTileViewModel(
constant([vm]),
constant(false),
constant("solid"),
)
}
targetWidth={300} targetWidth={300}
targetHeight={200} targetHeight={200}
expanded={false} expanded={false}
@@ -132,7 +144,13 @@ test("Screen share volume UI is hidden when screen share has no audio", async ()
const toggleExpanded = vi.fn(); const toggleExpanded = vi.fn();
const { container } = render( const { container } = render(
<SpotlightTile <SpotlightTile
vm={new SpotlightTileViewModel(constant([vm]), constant(false))} vm={
new SpotlightTileViewModel(
constant([vm]),
constant(false),
constant("solid"),
)
}
targetWidth={300} targetWidth={300}
targetHeight={200} targetHeight={200}
expanded={false} expanded={false}
@@ -168,7 +186,13 @@ test("SpotlightTile displays ringing media", async () => {
const toggleExpanded = vi.fn(); const toggleExpanded = vi.fn();
const { container } = render( const { container } = render(
<SpotlightTile <SpotlightTile
vm={new SpotlightTileViewModel(constant([vm]), constant(false))} vm={
new SpotlightTileViewModel(
constant([vm]),
constant(false),
constant("solid"),
)
}
targetWidth={300} targetWidth={300}
targetHeight={200} targetHeight={200}
expanded={false} expanded={false}

View File

@@ -65,6 +65,7 @@ interface SpotlightItemBaseProps {
displayName: string; displayName: string;
mxcAvatarUrl: string | undefined; mxcAvatarUrl: string | undefined;
showNameTags: boolean; showNameTags: boolean;
bgStyle: "solid" | "transparent";
focusable: boolean; focusable: boolean;
"aria-hidden"?: boolean; "aria-hidden"?: boolean;
} }
@@ -243,6 +244,7 @@ interface SpotlightItemProps {
targetHeight: number; targetHeight: number;
showNameTags: boolean; showNameTags: boolean;
showRingingStatus: boolean; showRingingStatus: boolean;
bgStyle: "solid" | "transparent";
focusable: boolean; focusable: boolean;
intersectionObserver$: Observable<IntersectionObserver>; intersectionObserver$: Observable<IntersectionObserver>;
/** /**
@@ -259,6 +261,7 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
targetHeight, targetHeight,
showNameTags, showNameTags,
showRingingStatus, showRingingStatus,
bgStyle,
focusable, focusable,
intersectionObserver$, intersectionObserver$,
snap, snap,
@@ -295,6 +298,7 @@ const SpotlightItem: FC<SpotlightItemProps> = ({
displayName, displayName,
mxcAvatarUrl, mxcAvatarUrl,
showNameTags, showNameTags,
bgStyle,
focusable, focusable,
"aria-hidden": ariaHidden, "aria-hidden": ariaHidden,
}; };
@@ -412,6 +416,7 @@ export const SpotlightTile: FC<Props> = ({
const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null); const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null);
const ref = useMergedRefs(ourRef, theirRef); const ref = useMergedRefs(ourRef, theirRef);
const maximised = useBehavior(vm.maximised$); const maximised = useBehavior(vm.maximised$);
const bgStyle = useBehavior(vm.bgStyle$);
const media = useBehavior(vm.media$); const media = useBehavior(vm.media$);
const [visibleId, setVisibleId] = useState<string | undefined>(media[0]?.id); const [visibleId, setVisibleId] = useState<string | undefined>(media[0]?.id);
const latestMedia = useLatest(media); const latestMedia = useLatest(media);
@@ -516,6 +521,7 @@ export const SpotlightTile: FC<Props> = ({
targetHeight={targetHeight} targetHeight={targetHeight}
showRingingStatus={showRingingStatus} showRingingStatus={showRingingStatus}
showNameTags={showNameTags} showNameTags={showNameTags}
bgStyle={bgStyle}
focusable={focusable} focusable={focusable}
intersectionObserver$={intersectionObserver$} intersectionObserver$={intersectionObserver$}
// This is how we get the container to scroll to the right media // This is how we get the container to scroll to the right media