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,
): [OneOnOnePortraitLayout, TileStore] {
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);
const tiles = update.build();

View File

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

View File

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

View File

@@ -39,12 +39,29 @@ class SpotlightTileData {
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 constructor(media: MediaViewModel[], maximised: boolean) {
public constructor(
media: MediaViewModel[],
maximised: boolean,
bgStyle: "solid" | "transparent",
) {
this.media$ = new BehaviorSubject(media);
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
* 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)
logger.debug(
`[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
if (this.prevSpotlight === null) {
this.spotlight = new SpotlightTileData(media, maximised);
this.spotlight = new SpotlightTileData(media, maximised, bgStyle);
} else {
this.spotlight = this.prevSpotlight;
this.spotlight.media = media;
this.spotlight.maximised = maximised;
this.spotlight.bgStyle = bgStyle;
}
}

View File

@@ -37,6 +37,7 @@ export class SpotlightTileViewModel {
public constructor(
public readonly media$: Behavior<MediaViewModel[]>,
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 {
grid-area: content;
background-color: var(--video-tile-background);
inline-size: 100%;
block-size: 100%;
border-radius: inherit;
contain: strict;
}
.media[data-bg-style="solid"] .bg {
background-color: var(--video-tile-background);
}
.avatar {
position: absolute;
top: 50%;

View File

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

View File

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

View File

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