From 883fb622e673204f822649372f827b3c63fdb6f9 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 9 Apr 2026 20:47:35 +0100 Subject: [PATCH] implement BrandApi and it's titleRenderer --- .../src/components/structures/MatrixChat.tsx | 41 +++++++++++++++---- apps/web/src/modules/Api.ts | 2 + apps/web/src/modules/BrandApi.ts | 27 ++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/modules/BrandApi.ts diff --git a/apps/web/src/components/structures/MatrixChat.tsx b/apps/web/src/components/structures/MatrixChat.tsx index ce5bfd4598b..f82683b5518 100644 --- a/apps/web/src/components/structures/MatrixChat.tsx +++ b/apps/web/src/components/structures/MatrixChat.tsx @@ -248,6 +248,9 @@ export default class MatrixChat extends React.PureComponent { // What to focus on next component update, if anything private focusNext: FocusNextType; private subTitleStatus: string; + private notificationCount = 0; + private hasActivity = false; + private errorDidOccur = false; private prevWindowWidth: number; private readonly loggedInView = createRef(); @@ -2081,17 +2084,36 @@ export default class MatrixChat extends React.PureComponent { } private setPageSubtitle(subtitle = ""): void { - if (this.state.currentRoomId) { - const client = MatrixClientPeg.get(); - const room = client?.getRoom(this.state.currentRoomId); - if (room) { - subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`; + const brand = SdkConfig.get().brand; + const room = this.state.currentRoomId + ? MatrixClientPeg.get()?.getRoom(this.state.currentRoomId) + : undefined; + + const customTitle = ModuleApi.instance.brand.renderTitle({ + brand, + notificationCount: this.notificationCount || undefined, + hasActivity: this.hasActivity || undefined, + statusText: this.errorDidOccur ? _t("common|offline") : undefined, + roomId: this.state.currentRoomId ?? undefined, + roomName: room?.name, + }); + + if (customTitle !== undefined) { + if (document.title !== customTitle) { + document.title = customTitle; } + return; + } + + const elementSuffix = brand !== "Element" ? " | Element" : ""; + + if (room) { + subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`; } else { subtitle = `${this.subTitleStatus} ${subtitle}`; } - const title = `${SdkConfig.get().brand} ${subtitle}`; + const title = `${brand}${elementSuffix} ${subtitle}`; if (document.title !== title) { document.title = title; @@ -2107,12 +2129,15 @@ export default class MatrixChat extends React.PureComponent { } this.subTitleStatus = ""; - if (state === SyncState.Error) { + this.errorDidOccur = state === SyncState.Error; + if (this.errorDidOccur) { this.subTitleStatus += `[${_t("common|offline")}] `; } + this.notificationCount = numUnreadRooms; + this.hasActivity = notificationState.level >= NotificationLevel.Activity; if (numUnreadRooms > 0) { this.subTitleStatus += `[${numUnreadRooms}]`; - } else if (notificationState.level >= NotificationLevel.Activity) { + } else if (this.hasActivity) { this.subTitleStatus += `*`; } diff --git a/apps/web/src/modules/Api.ts b/apps/web/src/modules/Api.ts index bb3c7497d52..9e10a48848d 100644 --- a/apps/web/src/modules/Api.ts +++ b/apps/web/src/modules/Api.ts @@ -33,6 +33,7 @@ import { StoresApi } from "./StoresApi.ts"; import { WidgetLifecycleApi } from "./WidgetLifecycleApi.ts"; import { WidgetApi } from "./WidgetApi.ts"; import { CustomisationsApi } from "./customisationsApi.ts"; +import { ElementWebBrandApi } from "./BrandApi.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false; @@ -87,6 +88,7 @@ export class ModuleApi implements Api { public readonly i18n = new I18nApi(); public readonly customComponents = new CustomComponentsApi(); public readonly customisations = new CustomisationsApi(); + public readonly brand = new ElementWebBrandApi(); public readonly extras = new ElementWebExtrasApi(); public readonly builtins = new ElementWebBuiltinsApi(); public readonly widgetLifecycle = new WidgetLifecycleApi(); diff --git a/apps/web/src/modules/BrandApi.ts b/apps/web/src/modules/BrandApi.ts new file mode 100644 index 00000000000..74972a273f3 --- /dev/null +++ b/apps/web/src/modules/BrandApi.ts @@ -0,0 +1,27 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type BrandApi, type TitleRenderFunction, type TitleRenderOptions } from "@element-hq/element-web-module-api"; + +export class ElementWebBrandApi implements BrandApi { + private titleRenderer: TitleRenderFunction | undefined; + + public registerTitleRenderer(renderFunction: TitleRenderFunction): void { + if (this.titleRenderer) { + throw new Error("A title renderer has already been registered by another module"); + } + this.titleRenderer = renderFunction; + } + + /** + * Render the window title using the registered renderer, or return undefined + * to fall back to the default title logic. + */ + public renderTitle(opts: TitleRenderOptions): string | undefined { + return this.titleRenderer?.(opts); + } +}