From 131b28ede8fcdf6b65227a2e383bd948a6988267 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 13 Jan 2025 11:41:29 +0000 Subject: [PATCH 1/8] New config options. --- docs/config.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/config.md b/docs/config.md index 8ca4ba4eb8b..65e8291bb6a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -211,6 +211,16 @@ Starting with `branding`, the following subproperties are available: 2. `auth_header_logo_url`: A URL to the logo used on the login, registration, etc pages. 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. +5. `title_template_in_room`: A template string that can be used to configure the title of the application. This applies while + the client is viewing a Matrix room. + + +#### `title_template` vars + +- `subtitle` +- `room_name` +- `brand` `embedded_pages` can be configured as such: From f5402b4ec4c0dbd71fb4d9648afd942265f995e2 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 13 Jan 2025 12:02:41 +0000 Subject: [PATCH 2/8] Add ability to customize the title template in branding. --- docs/config.md | 16 ++++++++----- src/IConfigOptions.ts | 2 ++ src/components/structures/MatrixChat.tsx | 29 +++++++++++++++++++----- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/docs/config.md b/docs/config.md index 65e8291bb6a..cd4eddcaefc 100644 --- a/docs/config.md +++ b/docs/config.md @@ -211,16 +211,20 @@ Starting with `branding`, the following subproperties are available: 2. `auth_header_logo_url`: A URL to the logo used on the login, registration, etc pages. 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. -5. `title_template_in_room`: A template string that can be used to configure the title of the application. This applies while - the client is viewing a Matrix room. +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 -- `subtitle` -- `room_name` -- `brand` +- `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: diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index bbb377e07b7..bbed4c9722d 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -50,6 +50,8 @@ 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 9d3114c67cb..ea65224454d 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -226,6 +226,9 @@ export default class MatrixChat extends React.PureComponent { private subTitleStatus: string; private prevWindowWidth: number; + private readonly titleTemplate: string; + private readonly titleTemplateInRoom: string; + private readonly loggedInView = createRef(); private dispatcherRef?: string; private themeWatcher?: ThemeWatcher; @@ -279,6 +282,9 @@ 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.titleTemplate = props.config.branding?.title_template ?? '$brand $status'; + this.titleTemplateInRoom = props.config.branding?.title_template_in_room ?? '$brand $status | $room_name'; } /** @@ -1941,21 +1947,32 @@ export default class MatrixChat extends React.PureComponent { }); } - private setPageSubtitle(subtitle = ""): void { + private setPageSubtitle(): void { + const params: { + $brand: string, + $status: string, + $room_name: string|undefined, + } = { + $brand: SdkConfig.get().brand, + $status: this.subTitleStatus, + $room_name: undefined, + }; + if (this.state.currentRoomId) { const client = MatrixClientPeg.get(); const room = client?.getRoom(this.state.currentRoomId); if (room) { - subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`; + params.$room_name = room.name; } - } else { - subtitle = `${this.subTitleStatus} ${subtitle}`; } + + const titleTemplate = params.$room_name ? this.titleTemplateInRoom : this.titleTemplate; - const title = `${SdkConfig.get().brand} ${subtitle}`; + const title = Object.entries(params).reduce( + (title: string, [key, value]) => title.replaceAll(key, (value ?? '').replaceAll('$', '$_DLR$')), titleTemplate); if (document.title !== title) { - document.title = title; + document.title = title.replaceAll('$_DLR$', '$'); } } From a8c170f8be8ddf9c65a9fb994378b73cb0a919c4 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 13 Jan 2025 13:12:04 +0000 Subject: [PATCH 3/8] Add tests --- playwright/e2e/branding/title.spec.ts | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 playwright/e2e/branding/title.spec.ts diff --git a/playwright/e2e/branding/title.spec.ts b/playwright/e2e/branding/title.spec.ts new file mode 100644 index 00000000000..71cb864aad0 --- /dev/null +++ b/playwright/e2e/branding/title.spec.ts @@ -0,0 +1,46 @@ +/* +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 { expect, test } from "../../element-web-test"; + +/* + * Tests for branding configuration + **/ + +test.describe('Test without branding config', () => { + test("Shows standard branding when showing the home page", async ({ pageWithCredentials: page }) => { + await page.goto("/"); + await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); + expect(page.title()).toEqual('Element *'); + }); + test("Shows standard branding when showing a room", async ({ app, pageWithCredentials: page }) => { + await app.client.createRoom({ name: "Test Room" }); + await app.viewRoomByName("Test Room"); + expect(page.title()).toEqual('Element * | Test Room'); + }); +}); + +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 }) => { + await page.goto("/"); + await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); + expect(page.title()).toEqual('TestingApp TestBrand * $ignoredParameter'); + }); + test("Shows custom branding when showing a room", async ({ app, pageWithCredentials: page }) => { + await app.client.createRoom({ name: "Test Room" }); + await app.viewRoomByName("Test Room"); + expect(page.title()).toEqual('TestingApp TestBrand * Test Room $ignoredParameter'); + }); +}); + From 9cccbeb799ece5e64658c4f66125dd4236d13895 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 13 Jan 2025 13:12:18 +0000 Subject: [PATCH 4/8] clear current room in two more contexts. --- src/components/structures/MatrixChat.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index ea65224454d..eb59675d0c2 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1109,6 +1109,7 @@ export default class MatrixChat extends React.PureComponent { } this.setStateForNewView({ view: Views.WELCOME, + currentRoomId: null, }); this.notifyNewScreen("welcome"); ThemeController.isLogin = true; @@ -1118,6 +1119,7 @@ export default class MatrixChat extends React.PureComponent { private viewLogin(otherState?: any): void { this.setStateForNewView({ view: Views.LOGIN, + currentRoomId: null, ...otherState, }); this.notifyNewScreen("login"); @@ -1949,9 +1951,9 @@ export default class MatrixChat extends React.PureComponent { private setPageSubtitle(): void { const params: { - $brand: string, - $status: string, - $room_name: string|undefined, + $brand: string; + $status: string; + $room_name: string|undefined; } = { $brand: SdkConfig.get().brand, $status: this.subTitleStatus, From 80cc0a928f5e5a395ed1b3e11d255ac75c2cf40c Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 13 Jan 2025 13:16:53 +0000 Subject: [PATCH 5/8] dollars --- docs/config.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/config.md b/docs/config.md index cd4eddcaefc..36362111f52 100644 --- a/docs/config.md +++ b/docs/config.md @@ -218,9 +218,9 @@ Starting with `branding`, the following subproperties are available: #### `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. +- `$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. From 0d576b217b2707441060c5829e49be9cc48d5b53 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 13 Jan 2025 13:19:02 +0000 Subject: [PATCH 6/8] lint --- docs/config.md | 11 ++++----- playwright/e2e/branding/title.spec.ts | 29 ++++++++++++------------ src/components/structures/MatrixChat.tsx | 14 +++++++----- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/docs/config.md b/docs/config.md index 36362111f52..24c2d60e792 100644 --- a/docs/config.md +++ b/docs/config.md @@ -212,7 +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 @@ -220,11 +219,11 @@ Starting with `branding`, the following subproperties are available: - `$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. +- `$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: diff --git a/playwright/e2e/branding/title.spec.ts b/playwright/e2e/branding/title.spec.ts index 71cb864aad0..2a2eb1593ab 100644 --- a/playwright/e2e/branding/title.spec.ts +++ b/playwright/e2e/branding/title.spec.ts @@ -11,36 +11,37 @@ import { expect, test } from "../../element-web-test"; * Tests for branding configuration **/ -test.describe('Test without branding config', () => { +test.describe("Test without branding config", () => { test("Shows standard branding when showing the home page", async ({ pageWithCredentials: page }) => { await page.goto("/"); await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); - expect(page.title()).toEqual('Element *'); + expect(page.title()).toEqual("Element *"); }); test("Shows standard branding when showing a room", async ({ app, pageWithCredentials: page }) => { await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); - expect(page.title()).toEqual('Element * | Test Room'); + expect(page.title()).toEqual("Element * | Test Room"); }); }); -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.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 }) => { await page.goto("/"); await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); - expect(page.title()).toEqual('TestingApp TestBrand * $ignoredParameter'); + expect(page.title()).toEqual("TestingApp TestBrand * $ignoredParameter"); }); test("Shows custom branding when showing a room", async ({ app, pageWithCredentials: page }) => { await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); - expect(page.title()).toEqual('TestingApp TestBrand * Test Room $ignoredParameter'); + expect(page.title()).toEqual("TestingApp TestBrand * Test Room $ignoredParameter"); }); }); - diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index eb59675d0c2..9ab5aad3988 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -283,8 +283,8 @@ export default class MatrixChat extends React.PureComponent { // 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'; + this.titleTemplate = props.config.branding?.title_template ?? "$brand $status"; + this.titleTemplateInRoom = props.config.branding?.title_template_in_room ?? "$brand $status | $room_name"; } /** @@ -1953,7 +1953,7 @@ export default class MatrixChat extends React.PureComponent { const params: { $brand: string; $status: string; - $room_name: string|undefined; + $room_name: string | undefined; } = { $brand: SdkConfig.get().brand, $status: this.subTitleStatus, @@ -1967,14 +1967,16 @@ export default class MatrixChat extends React.PureComponent { params.$room_name = room.name; } } - + const titleTemplate = params.$room_name ? this.titleTemplateInRoom : this.titleTemplate; const title = Object.entries(params).reduce( - (title: string, [key, value]) => title.replaceAll(key, (value ?? '').replaceAll('$', '$_DLR$')), titleTemplate); + (title: string, [key, value]) => title.replaceAll(key, (value ?? "").replaceAll("$", "$_DLR$")), + titleTemplate, + ); if (document.title !== title) { - document.title = title.replaceAll('$_DLR$', '$'); + document.title = title.replaceAll("$_DLR$", "$"); } } From b589757c3444cc7343db756d1dd39ab943c3c699 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 17 Jan 2025 11:47:37 +0000 Subject: [PATCH 7/8] Replace with module API. --- docs/config.md | 13 ---- playwright/e2e/branding/title.spec.ts | 4 - src/IConfigOptions.ts | 2 - src/components/structures/MatrixChat.tsx | 97 +++++++++++++----------- src/modules/ModuleRunner.ts | 24 ++++++ 5 files changed, 76 insertions(+), 64 deletions(-) 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 9ab5aad3988..f92121f9507 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -131,6 +131,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"; @@ -223,18 +224,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; @@ -278,13 +277,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"; } /** @@ -1109,7 +1101,6 @@ export default class MatrixChat extends React.PureComponent { } this.setStateForNewView({ view: Views.WELCOME, - currentRoomId: null, }); this.notifyNewScreen("welcome"); ThemeController.isLogin = true; @@ -1119,7 +1110,6 @@ export default class MatrixChat extends React.PureComponent { private viewLogin(otherState?: any): void { this.setStateForNewView({ view: Views.LOGIN, - currentRoomId: null, ...otherState, }); this.notifyNewScreen("login"); @@ -1482,7 +1472,7 @@ export default class MatrixChat extends React.PureComponent { collapseLhs: false, currentRoomId: null, }); - this.subTitleStatus = ""; + this.subtitleContext = undefined; this.setPageSubtitle(); this.stores.onLoggedOut(); } @@ -1498,7 +1488,7 @@ export default class MatrixChat extends React.PureComponent { collapseLhs: false, currentRoomId: null, }); - this.subTitleStatus = ""; + this.subtitleContext = undefined; this.setPageSubtitle(); } @@ -1950,33 +1940,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; } } @@ -1987,17 +2000,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.`, + ); + } + } } } From bcf9854a4c06be574f4cf6969c39c8457bd7e2ed Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 17 Jan 2025 12:14:46 +0000 Subject: [PATCH 8/8] Fix so that we still use notificationsMuted / unread even if a room isn't in view. --- src/components/structures/MatrixChat.tsx | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index f92121f9507..f7b4473cd4a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1944,20 +1944,18 @@ export default class MatrixChat extends React.PureComponent { let context: AppTitleContext = { brand: SdkConfig.get().brand, syncError: extraContext?.syncState === SyncState.Error, + notificationsMuted: extraContext && extraContext.userNotificationLevel < NotificationLevel.Activity, + unreadNotificationCount: extraContext?.unreadNotificationCount, }; - 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, - }; - } + if (this.state.currentRoomId) { + const client = MatrixClientPeg.get(); + const room = client?.getRoom(this.state.currentRoomId); + context = { + ...context, + roomId: this.state.currentRoomId, + roomName: room?.name, + }; } const moduleTitle = ModuleRunner.instance.extensions.branding?.getAppTitle(context); @@ -1968,13 +1966,15 @@ export default class MatrixChat extends React.PureComponent { return; } + // Use application default. + let subtitle = ""; if (context?.syncError) { subtitle += `[${_t("common|offline")}] `; } - if ('unreadNotificationCount' in context && context.unreadNotificationCount > 0) { + if (context.unreadNotificationCount !== undefined && context.unreadNotificationCount > 0) { subtitle += `[${context.unreadNotificationCount}]`; - } else if ('notificationsMuted' in context && !context.notificationsMuted) { + } else if (context.notificationsMuted !== undefined && !context.notificationsMuted) { subtitle += `*`; }