From 9d21581259b4243e833ec2ca3fc786ea1e1a8e89 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 30 Jun 2026 16:28:26 +0200 Subject: [PATCH 01/11] Dismiss sections release announcement in end-to-end tests --- playwright/widget/test-helpers.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index 023c9f2b4..372394ac8 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -188,6 +188,12 @@ export class TestHelpers { break; } } + + // Also dismiss the release announcement + await page + .getByRole("dialog", { name: "Introducing Sections" }) + .getByRole("button", { name: "OK" }) + .click(); } public static async createRoom( From 8c6a16f3d7efea93dc93e54c1c8df12ea62dd1b8 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 25 Jun 2026 18:16:27 +0200 Subject: [PATCH 02/11] Add URL param to control background style --- docs/url_params.md | 1 + src/App.tsx | 18 ++++++++---------- src/UrlParams.ts | 12 ++++++++++++ src/index.css | 8 ++++++-- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/url_params.md b/docs/url_params.md index 7b80690a4..789e3f3ce 100644 --- a/docs/url_params.md +++ b/docs/url_params.md @@ -67,6 +67,7 @@ These parameters are relevant to both [widget](./embedded_standalone.md) and [st | `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. | | `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) | | `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. | +| `background` | One of: `solid`, `gradient` | No, defaults to `gradient` | No, defaults to `gradient` | Visual style of the page background. | | `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. | | `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. | | `autoLeave` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. | diff --git a/src/App.tsx b/src/App.tsx index b87f587ca..8f6ef21a1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,7 @@ import { useTheme } from "./useTheme"; import { ProcessorProvider } from "./livekit/TrackProcessorContext"; import { type AppViewModel } from "./state/AppViewModel"; import { MediaDevicesContext } from "./MediaDevicesContext"; -import { getUrlParams, HeaderStyle } from "./UrlParams"; +import { getUrlParams, HeaderStyle, useUrlParams } from "./UrlParams"; import { AppBar } from "./AppBar"; const SentryRoute = Sentry.withSentryReactRouterV7Routing(Route); @@ -41,19 +41,17 @@ interface SimpleProviderProps { const BackgroundProvider: FC = ({ children }) => { const { pathname } = useLocation(); + const { background } = useUrlParams(); useEffect(() => { - let backgroundImage = ""; - if (!["/login", "/register"].includes(pathname) && !widget) { - backgroundImage = "var(--background-gradient)"; - } + document + .getElementsByTagName("body")[0] + .setAttribute("data-background", background); + }, [pathname, background]); - document.getElementsByTagName("body")[0].style.backgroundImage = - backgroundImage; - }, [pathname]); - - return <>{children}; + return children; }; + const ThemeProvider: FC = ({ children }) => { useTheme(); return children; diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 773aa4121..805cab710 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -45,6 +45,11 @@ export enum HeaderStyle { AppBar = "app_bar", } +export enum BackgroundStyle { + Solid = "solid", + Gradient = "gradient", +} + /** * The UrlProperties are used to pass required data to the widget. * Those are different in different rooms, users, devices. They do not configure the behavior of the @@ -145,6 +150,10 @@ export interface UrlProperties { * can be "light", "dark", "light-high-contrast" or "dark-high-contrast". */ theme: string | null; + /** + * The visual style of the page background. + */ + background: BackgroundStyle; } /** @@ -452,6 +461,9 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => { fonts: parser.getAllParams("font"), fontScale: Number.isNaN(fontScale) ? null : fontScale, theme: parser.getParam("theme"), + background: + parser.getEnumParam("background", BackgroundStyle) ?? + BackgroundStyle.Gradient, viaServers: !isWidget ? parser.getParam("viaServers") : null, homeserver: !isWidget ? parser.getParam("homeserver") : null, posthogApiHost: parser.getParam("posthogApiHost"), diff --git a/src/index.css b/src/index.css index 039b2a421..5468612d7 100644 --- a/src/index.css +++ b/src/index.css @@ -74,9 +74,7 @@ layer(compound); body { background-color: var(--cpd-color-bg-canvas-default); - background-size: calc(max(1440px, 100vw)) calc(max(800px, 100vh)); background-repeat: no-repeat; - background-position: center; color: var(--cpd-color-text-primary); color-scheme: dark; margin: 0; @@ -85,6 +83,12 @@ body { -webkit-tap-highlight-color: transparent; } +body[data-background="gradient"] { + background-image: var(--background-gradient); + background-size: calc(max(1440px, 100vw)) calc(max(800px, 100vh)); + background-position: center; +} + /* This prohibits the view to scroll for pages smaller than 122px in width we use this for mobile pip webviews */ .no-scroll-body { From 360a4ff0269da9281dc72e594ab1a5f713503344 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 25 Jun 2026 18:16:37 +0200 Subject: [PATCH 03/11] Add mobile gradient background --- ...roundGradient.svg => desktop-gradient.svg} | 0 src/graphics/loggedOutGradient.svg | 48 ----------- src/graphics/mobile-gradient.svg | 86 +++++++++++++++++++ src/index.css | 16 +++- 4 files changed, 98 insertions(+), 52 deletions(-) rename src/graphics/{backgroundGradient.svg => desktop-gradient.svg} (100%) delete mode 100644 src/graphics/loggedOutGradient.svg create mode 100644 src/graphics/mobile-gradient.svg diff --git a/src/graphics/backgroundGradient.svg b/src/graphics/desktop-gradient.svg similarity index 100% rename from src/graphics/backgroundGradient.svg rename to src/graphics/desktop-gradient.svg diff --git a/src/graphics/loggedOutGradient.svg b/src/graphics/loggedOutGradient.svg deleted file mode 100644 index 7855ef81c..000000000 --- a/src/graphics/loggedOutGradient.svg +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/graphics/mobile-gradient.svg b/src/graphics/mobile-gradient.svg new file mode 100644 index 000000000..f6e8b9161 --- /dev/null +++ b/src/graphics/mobile-gradient.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/index.css b/src/index.css index 5468612d7..6cc094a91 100644 --- a/src/index.css +++ b/src/index.css @@ -55,7 +55,6 @@ layer(compound); --small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15); --big-drop-shadow: 0px 0px 24px 0px #1b1d221a; --subtle-drop-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); - --background-gradient: url("graphics/backgroundGradient.svg"); --call-view-overlay-layer: 1; --call-view-header-footer-layer: 2; @@ -74,7 +73,6 @@ layer(compound); body { background-color: var(--cpd-color-bg-canvas-default); - background-repeat: no-repeat; color: var(--cpd-color-text-primary); color-scheme: dark; margin: 0; @@ -83,8 +81,18 @@ body { -webkit-tap-highlight-color: transparent; } -body[data-background="gradient"] { - background-image: var(--background-gradient); +body[data-background="gradient"]::before { + content: ""; + position: fixed; + inset: 0; + background-image: url("graphics/mobile-gradient.svg"); + background-size: auto; + background-position: bottom; + background-repeat: no-repeat; +} + +body[data-background="gradient"][data-platform="desktop"]::before { + background-image: url("graphics/desktop-gradient.svg"); background-size: calc(max(1440px, 100vw)) calc(max(800px, 100vh)); background-position: center; } From 8fa3f33f379708ee8eb9bf5da377e1835770282e Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 25 Jun 2026 18:22:43 +0200 Subject: [PATCH 04/11] Let page background shine through spotlight tiles By giving spotlight tiles a transparent background in certain layouts. --- src/state/OneOnOnePortraitLayout.ts | 2 +- src/state/PipLayout.ts | 1 + src/state/SpotlightExpandedLayout.ts | 2 +- src/state/TileStore.ts | 30 ++++++++++++++++++++++---- src/state/TileViewModel.ts | 1 + src/tile/MediaView.module.css | 5 ++++- src/tile/MediaView.tsx | 3 +++ src/tile/SpotlightTile.test.tsx | 32 ++++++++++++++++++++++++---- src/tile/SpotlightTile.tsx | 6 ++++++ 9 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/state/OneOnOnePortraitLayout.ts b/src/state/OneOnOnePortraitLayout.ts index 9be80421b..98314a039 100644 --- a/src/state/OneOnOnePortraitLayout.ts +++ b/src/state/OneOnOnePortraitLayout.ts @@ -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(); diff --git a/src/state/PipLayout.ts b/src/state/PipLayout.ts index 6ac1e4f02..0cd6a77cb 100644 --- a/src/state/PipLayout.ts +++ b/src/state/PipLayout.ts @@ -21,6 +21,7 @@ export function pipLayout( update.registerSpotlight( media.spotlight, platform === "desktop" ? false : true, + "transparent", ); const tiles = update.build(); return [ diff --git a/src/state/SpotlightExpandedLayout.ts b/src/state/SpotlightExpandedLayout.ts index 59ab8ab98..45a37ca29 100644 --- a/src/state/SpotlightExpandedLayout.ts +++ b/src/state/SpotlightExpandedLayout.ts @@ -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(); diff --git a/src/state/TileStore.ts b/src/state/TileStore.ts index 3cbed2809..5d4df462e 100644 --- a/src/state/TileStore.ts +++ b/src/state/TileStore.ts @@ -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; } } diff --git a/src/state/TileViewModel.ts b/src/state/TileViewModel.ts index 81aac8298..bb96a16b8 100644 --- a/src/state/TileViewModel.ts +++ b/src/state/TileViewModel.ts @@ -37,6 +37,7 @@ export class SpotlightTileViewModel { public constructor( public readonly media$: Behavior, public readonly maximised$: Behavior, + public readonly bgStyle$: Behavior<"solid" | "transparent">, ) {} } diff --git a/src/tile/MediaView.module.css b/src/tile/MediaView.module.css index 5e7b3b28d..5b68210c8 100644 --- a/src/tile/MediaView.module.css +++ b/src/tile/MediaView.module.css @@ -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%; diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index f860f41eb..9891e8218 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -43,6 +43,7 @@ interface Props extends ComponentProps { 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 = ({ displayName, mxcAvatarUrl, avatarStyle = "solid", + bgStyle = "solid", focusable, primaryButton, status, @@ -118,6 +120,7 @@ export const MediaView: FC = ({ ref={ref} data-testid="videoTile" data-video-fit={videoFit} + data-bg-style={bgStyle} {...props} >
diff --git a/src/tile/SpotlightTile.test.tsx b/src/tile/SpotlightTile.test.tsx index 69bdacf76..7924e557d 100644 --- a/src/tile/SpotlightTile.test.tsx +++ b/src/tile/SpotlightTile.test.tsx @@ -58,7 +58,13 @@ test("SpotlightTile is accessible", async () => { const toggleExpanded = vi.fn(); const { container } = render( const { container } = render(