diff --git a/docs/config.md b/docs/config.md index 24c2d60e792..8ca4ba4eb8b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -212,19 +212,6 @@ Starting with `branding`, the following subproperties are available: 3. `auth_footer_links`: A list of links to add to the footer during login, registration, etc. Each entry must have a `text` and `url` property. -4. `title_template`: A template string that can be used to configure the title of the application when not viewing a room. -5. `title_template_in_room`: A template string that can be used to configure the title of the application when viewing a room - -#### `title_template` vars - -- `$brand` The name of the web app, as configured by the `brand` config value. -- `$room_name` The friendly name of a room. Only applicable to `title_template_in_room`. -- `$status` The client's status, repesented as. - - The notification count, when at least one room is unread. - - "\*" when no rooms are unread, but notifications are not muted. - - "Offline", when the client is offline. - - "", when the client isn't logged in or notifications are muted. - `embedded_pages` can be configured as such: 1. `welcome_url`: A URL to an HTML page to show as a welcome page (landing on `#/welcome`). When not specified, the default diff --git a/playwright/e2e/branding/title.spec.ts b/playwright/e2e/branding/title.spec.ts index 2a2eb1593ab..bf9c4895363 100644 --- a/playwright/e2e/branding/title.spec.ts +++ b/playwright/e2e/branding/title.spec.ts @@ -28,10 +28,6 @@ test.describe("Test with custom branding", () => { test.use({ config: { brand: "TestBrand", - branding: { - title_template: "TestingApp $ignoredParameter $brand $status $ignoredParameter", - title_template_in_room: "TestingApp $brand $status $room_name $ignoredParameter", - }, }, }); test("Shows custom branding when showing the home page", async ({ pageWithCredentials: page }) => { diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index bbed4c9722d..bbb377e07b7 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -50,8 +50,6 @@ export interface IConfigOptions { welcome_background_url?: string | string[]; // chosen at random if array auth_header_logo_url?: string; auth_footer_links?: { text: string; url: string }[]; - title_template?: string; - title_template_in_room?: string; }; force_verification?: boolean; // if true, users must verify new logins diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 14990c9eecc..254f2287abe 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -133,6 +133,7 @@ import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView" import { LoginSplashView } from "./auth/LoginSplashView"; import { cleanUpDraftsIfRequired } from "../../DraftCleaner"; import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore"; +import { AppTitleContext } from "@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions"; // legacy export export { default as Views } from "../../Views"; @@ -225,18 +226,16 @@ 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 prevWindowWidth: number; - private readonly titleTemplate: string; - private readonly titleTemplateInRoom: string; - private readonly loggedInView = createRef(); private dispatcherRef?: string; private themeWatcher?: ThemeWatcher; private fontWatcher?: FontWatcher; private readonly stores: SdkContextClass; + private subtitleContext?: {unreadNotificationCount: number, userNotificationLevel: NotificationLevel, syncState: SyncState}; + public constructor(props: IProps) { super(props); this.stores = SdkContextClass.instance; @@ -280,13 +279,6 @@ export default class MatrixChat extends React.PureComponent { } this.prevWindowWidth = UIStore.instance.windowWidth || 1000; - - // 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.titleTemplate = props.config.branding?.title_template ?? "$brand $status"; - this.titleTemplateInRoom = props.config.branding?.title_template_in_room ?? "$brand $status | $room_name"; } /** @@ -1112,7 +1104,6 @@ export default class MatrixChat extends React.PureComponent { } this.setStateForNewView({ view: Views.WELCOME, - currentRoomId: null, }); this.notifyNewScreen("welcome"); ThemeController.isLogin = true; @@ -1122,7 +1113,6 @@ export default class MatrixChat extends React.PureComponent { private viewLogin(otherState?: any): void { this.setStateForNewView({ view: Views.LOGIN, - currentRoomId: null, ...otherState, }); this.notifyNewScreen("login"); @@ -1490,7 +1480,7 @@ export default class MatrixChat extends React.PureComponent { collapseLhs: false, currentRoomId: null, }); - this.subTitleStatus = ""; + this.subtitleContext = undefined; this.setPageSubtitle(); this.stores.onLoggedOut(); } @@ -1506,7 +1496,7 @@ export default class MatrixChat extends React.PureComponent { collapseLhs: false, currentRoomId: null, }); - this.subTitleStatus = ""; + this.subtitleContext = undefined; this.setPageSubtitle(); } @@ -1958,33 +1948,56 @@ export default class MatrixChat extends React.PureComponent { } private setPageSubtitle(): void { - const params: { - $brand: string; - $status: string; - $room_name: string | undefined; - } = { - $brand: SdkConfig.get().brand, - $status: this.subTitleStatus, - $room_name: undefined, + const extraContext = this.subtitleContext; + let context: AppTitleContext = { + brand: SdkConfig.get().brand, + syncError: extraContext?.syncState === SyncState.Error, }; - if (this.state.currentRoomId) { - const client = MatrixClientPeg.get(); - const room = client?.getRoom(this.state.currentRoomId); - if (room) { - params.$room_name = room.name; + if (extraContext) { + if (this.state.currentRoomId) { + const client = MatrixClientPeg.get(); + const room = client?.getRoom(this.state.currentRoomId); + context = { + ...context, + roomId: this.state.currentRoomId, + roomName: room?.name, + notificationsMuted: extraContext.userNotificationLevel < NotificationLevel.Activity, + unreadNotificationCount: extraContext.unreadNotificationCount, + }; } } - const titleTemplate = params.$room_name ? this.titleTemplateInRoom : this.titleTemplate; + const moduleTitle = ModuleRunner.instance.extensions.branding?.getAppTitle(context); + if (moduleTitle) { + if (document.title !== moduleTitle) { + document.title = moduleTitle; + } + return; + } - const title = Object.entries(params).reduce( - (title: string, [key, value]) => title.replaceAll(key, (value ?? "").replaceAll("$", "$_DLR$")), - titleTemplate, - ); + let subtitle = ""; + if (context?.syncError) { + subtitle += `[${_t("common|offline")}] `; + } + if ('unreadNotificationCount' in context && context.unreadNotificationCount > 0) { + subtitle += `[${context.unreadNotificationCount}]`; + } else if ('notificationsMuted' in context && !context.notificationsMuted) { + subtitle += `*`; + } + + if ('roomId' in context && context.roomId) { + if (context.roomName) { + subtitle = `${subtitle} | ${context.roomName}`; + } + } else { + subtitle = subtitle; + } + + const title = `${SdkConfig.get().brand} ${subtitle}`; if (document.title !== title) { - document.title = title.replaceAll("$_DLR$", "$"); + document.title = title; } } @@ -1995,17 +2008,11 @@ export default class MatrixChat extends React.PureComponent { PlatformPeg.get()!.setErrorStatus(state === SyncState.Error); PlatformPeg.get()!.setNotificationCount(numUnreadRooms); } - - 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.subtitleContext = { + syncState: state, + userNotificationLevel: notificationState.level, + unreadNotificationCount: numUnreadRooms, + }; this.setPageSubtitle(); }; diff --git a/src/modules/ModuleRunner.ts b/src/modules/ModuleRunner.ts index c01015206dd..042976da19c 100644 --- a/src/modules/ModuleRunner.ts +++ b/src/modules/ModuleRunner.ts @@ -17,6 +17,10 @@ import { DefaultExperimentalExtensions, ProvideExperimentalExtensions, } from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions"; +import { + ProvideBrandingExtensions, +} from "@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions"; + import { AppModule } from "./AppModule"; import { ModuleFactory } from "./ModuleFactory"; @@ -30,6 +34,7 @@ class ExtensionsManager { // Private backing fields for extensions private cryptoSetupExtension: ProvideCryptoSetupExtensions; private experimentalExtension: ProvideExperimentalExtensions; + private brandingExtension?: ProvideBrandingExtensions; /** `true` if `cryptoSetupExtension` is the default implementation; `false` if it is implemented by a module. */ private hasDefaultCryptoSetupExtension = true; @@ -67,6 +72,15 @@ class ExtensionsManager { return this.experimentalExtension; } + /** + * Provides branding extension. + * + * @returns The registered extension. If no module provides this extension, undefined is returned.. + */ + public get branding(): ProvideBrandingExtensions|undefined { + return this.brandingExtension; + } + /** * Add any extensions provided by the module. * @@ -100,6 +114,16 @@ class ExtensionsManager { ); } } + + if (runtimeModule.extensions?.branding) { + if (!this.brandingExtension) { + this.brandingExtension = runtimeModule.extensions?.branding; + } else { + throw new Error( + `adding experimental branding implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`, + ); + } + } } }