From ced693c2560c988b4a62f7eea34ccb29da3bc02b Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 23 Jun 2025 13:08:07 +0100 Subject: [PATCH] Rebrand to the "brand" API and add title support. --- src/components/structures/MatrixChat.tsx | 68 ++++++++++++++++-------- src/favicon.ts | 2 +- src/modules/Api.ts | 4 +- src/modules/brandApi.ts | 58 ++++++++++++++++++++ src/modules/faviconApi.ts | 36 ------------- 5 files changed, 106 insertions(+), 62 deletions(-) create mode 100644 src/modules/brandApi.ts delete mode 100644 src/modules/faviconApi.ts diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index e61713ca69..e56f8c6045 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -141,6 +141,8 @@ import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenFor import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload"; import Markdown from "../../Markdown"; import { sanitizeHtmlParams } from "../../Linkify"; +import moduleApi from "../../modules/Api" +import { TitleRenderOptions } from "@element-hq/element-web-module-api"; // legacy export export { default as Views } from "../../Views"; @@ -227,7 +229,7 @@ export default class MatrixChat extends React.PureComponent { private tokenLogin?: boolean; // What to focus on next component update, if anything private focusNext: FocusNextType; - private subTitleStatus: string; + private subTitleState: TitleRenderOptions; private prevWindowWidth: number; private readonly loggedInView = createRef(); @@ -283,7 +285,7 @@ export default class MatrixChat extends React.PureComponent { // object field used for tracking the status info appended to the title tag. // we don't do it as react state as i'm scared about triggering needless react refreshes. - this.subTitleStatus = ""; + this.subTitleState = {}; } /** @@ -1505,7 +1507,7 @@ export default class MatrixChat extends React.PureComponent { collapseLhs: false, currentRoomId: null, }); - this.subTitleStatus = ""; + this.subTitleState = {}; this.setPageSubtitle(); this.stores.onLoggedOut(); } @@ -1521,7 +1523,7 @@ export default class MatrixChat extends React.PureComponent { collapseLhs: false, currentRoomId: null, }); - this.subTitleStatus = ""; + this.subTitleState = {}; this.setPageSubtitle(); } @@ -1991,19 +1993,43 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({ action: "message_sent" }); }); } + - private setPageSubtitle(subtitle = ""): void { + private setPageSubtitle(): void { + let roomName: string|undefined; if (this.state.currentRoomId) { const client = MatrixClientPeg.get(); - const room = client?.getRoom(this.state.currentRoomId); - if (room) { - subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`; - } - } else { - subtitle = `${this.subTitleStatus} ${subtitle}`; + roomName = client?.getRoom(this.state.currentRoomId)?.name; + } + + let title = moduleApi.brandApi.renderTitle({...this.subTitleState, roomName, roomId: this.state.currentRoomId ?? undefined}); + + if (title === undefined) { + // No module API implemented, fallback + let subTitleStatus = ""; + + if (this.subTitleState.errorDidOccur) { + subTitleStatus += `[${_t("common|offline")}] ` + } + + if ((this.subTitleState.notificationCount ?? 0) > 0) { + subTitleStatus += `[${this.subTitleState.notificationCount}]`; + } else if (this.subTitleState.notificationsEnabled) { + subTitleStatus += `*`; + } + + let subtitle; + if (this.state.currentRoomId) { + if (roomName) { + subtitle = `${subTitleStatus} | ${roomName} ${subtitle}`; + } + } else { + subtitle = `${subTitleStatus} ${subtitle}`; + } + + title = `${SdkConfig.get().brand} ${subtitle}`; } - const title = `${SdkConfig.get().brand} ${subtitle}`; if (document.title !== title) { document.title = title; @@ -2011,22 +2037,18 @@ export default class MatrixChat extends React.PureComponent { } private onUpdateStatusIndicator = (notificationState: SummarizedNotificationState, state: SyncState): void => { - const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here + const notificationCount = notificationState.numUnreadStates; // we know that states === rooms here if (PlatformPeg.get()) { PlatformPeg.get()!.setErrorStatus(state === SyncState.Error); - PlatformPeg.get()!.setNotificationCount(numUnreadRooms); + PlatformPeg.get()!.setNotificationCount(notificationCount); } - this.subTitleStatus = ""; - if (state === SyncState.Error) { - this.subTitleStatus += `[${_t("common|offline")}] `; - } - if (numUnreadRooms > 0) { - this.subTitleStatus += `[${numUnreadRooms}]`; - } else if (notificationState.level >= NotificationLevel.Activity) { - this.subTitleStatus += `*`; - } + this.subTitleState = { + errorDidOccur: state === SyncState.Error, + notificationCount, + notificationsEnabled: notificationState.level >= NotificationLevel.Activity, + }; this.setPageSubtitle(); }; diff --git a/src/favicon.ts b/src/favicon.ts index bdaf4aafab..621e94c225 100644 --- a/src/favicon.ts +++ b/src/favicon.ts @@ -242,7 +242,7 @@ export default class Favicon { return; } - const badgeUrl = moduleApi.faviconApi.renderFavicon(opts) || this.renderBadge(opts); + const badgeUrl = moduleApi.brandApi.renderFavicon(opts) || this.renderBadge(opts); this.setIcon(badgeUrl); } diff --git a/src/modules/Api.ts b/src/modules/Api.ts index 8945e0d3c0..cea682c0c9 100644 --- a/src/modules/Api.ts +++ b/src/modules/Api.ts @@ -22,7 +22,7 @@ import { WidgetVariableCustomisations } from "../customisations/WidgetVariables. import { ConfigApi } from "./ConfigApi.ts"; import { I18nApi } from "./I18nApi.ts"; import { CustomComponentsApi } from "./customComponentApi.ts"; -import { FaviconApi } from "./faviconApi.ts"; +import { BrandApi } from "./brandApi.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false; @@ -62,7 +62,7 @@ class ModuleApi implements Api { public readonly i18n = new I18nApi(); public readonly customComponents = new CustomComponentsApi(); public readonly rootNode = document.getElementById("matrixchat")!; - public readonly faviconApi = new FaviconApi(); + public readonly brandApi = new BrandApi(); public createRoot(element: Element): Root { return createRoot(element); diff --git a/src/modules/brandApi.ts b/src/modules/brandApi.ts new file mode 100644 index 0000000000..24c1f0c70e --- /dev/null +++ b/src/modules/brandApi.ts @@ -0,0 +1,58 @@ +/* +Copyright 2025 New Vector 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 as IBrandApi, + FaviconRenderFunction, + FaviconRenderOptions, + TitleRenderFunction, + TitleRenderOptions +} from "@element-hq/element-web-module-api"; + + +export class BrandApi implements IBrandApi { + private registeredFaviconFunction?: FaviconRenderFunction; + private registeredTitleFunction?: TitleRenderFunction; + + public registerFaviconRenderer( + func: FaviconRenderFunction + ): void { + if (this.registeredFaviconFunction) { + throw Error('A custom favicon rendering function has already been registered'); + } + this.registeredFaviconFunction = func; + } + + public registerTitleRenderer( + func: TitleRenderFunction + ): void { + if (this.registeredTitleFunction) { + throw Error('A custom title rendering function has already been registered'); + } + this.registeredTitleFunction = func; + } + + /** + * Returns a URL to a rendered favicon if a module has generated one, otherwise + * this returns undefined. + * @param opts Options to pass to the render function. + * @returns A URL string, or undefined. + */ + public renderFavicon(opts: FaviconRenderOptions): string|undefined { + return this.registeredFaviconFunction?.(opts); + } + + /** + * Returns the title text if a module has generated one, otherwise + * this returns undefined. + * @param opts Options to pass to the render function. + * @returns Title text, or undefined. + */ + public renderTitle(opts: TitleRenderOptions): string|undefined { + return this.registeredTitleFunction?.(opts); + } +} diff --git a/src/modules/faviconApi.ts b/src/modules/faviconApi.ts deleted file mode 100644 index 809f79acc0..0000000000 --- a/src/modules/faviconApi.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2025 New Vector 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 { - FaviconApi as IFaviconApi, - FaviconRenderFunction, - FaviconRenderOptions -} from "@element-hq/element-web-module-api"; - - -export class FaviconApi implements IFaviconApi { - private registeredFunction?: FaviconRenderFunction; - - public registerRenderer( - func: FaviconRenderFunction - ): void { - if (this.registeredFunction) { - throw Error('A custom favicon rendering function has already been registered'); - } - this.registeredFunction = func; - } - - /** - * Returns a URL to a rendered favicon if a module has generated one, otherwise - * this returns undefined. - * @param opts Options to pass to the render function. - * @returns A URL string, or undefined. - */ - public renderFavicon(opts: FaviconRenderOptions): string|undefined { - return this.registeredFunction?.(opts); - } -}