Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Templatable document title (via Module API) #28979

Draft
wants to merge 8 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions playwright/e2e/branding/title.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
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 *");

Check failure on line 18 in playwright/e2e/branding/title.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › branding/title.spec.ts:15:9 › Test without branding config › Shows standard branding when showing the home page

1) [Chrome] › branding/title.spec.ts:15:9 › Test without branding config › Shows standard branding when showing the home page Error: expect(received).toEqual(expected) // deep equality Expected: "Element *" Received: {Symbol(async_id_symbol): 556924, Symbol(trigger_async_id_symbol): 547915, Symbol(kResourceStore): undefined} 16 | await page.goto("/"); 17 | await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); > 18 | expect(page.title()).toEqual("Element *"); | ^ 19 | }); 20 | test("Shows standard branding when showing a room", async ({ app, pageWithCredentials: page }) => { 21 | await app.client.createRoom({ name: "Test Room" }); at /home/runner/work/element-web/element-web/playwright/e2e/branding/title.spec.ts:18:30

Check failure on line 18 in playwright/e2e/branding/title.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › branding/title.spec.ts:15:9 › Test without branding config › Shows standard branding when showing the home page

1) [Chrome] › branding/title.spec.ts:15:9 › Test without branding config › Shows standard branding when showing the home page Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality Expected: "Element *" Received: {Symbol(async_id_symbol): 31774, Symbol(trigger_async_id_symbol): 15486, Symbol(kResourceStore): undefined} 16 | await page.goto("/"); 17 | await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); > 18 | expect(page.title()).toEqual("Element *"); | ^ 19 | }); 20 | test("Shows standard branding when showing a room", async ({ app, pageWithCredentials: page }) => { 21 | await app.client.createRoom({ name: "Test Room" }); at /home/runner/work/element-web/element-web/playwright/e2e/branding/title.spec.ts:18:30

Check failure on line 18 in playwright/e2e/branding/title.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › branding/title.spec.ts:15:9 › Test without branding config › Shows standard branding when showing the home page

1) [Chrome] › branding/title.spec.ts:15:9 › Test without branding config › Shows standard branding when showing the home page Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality Expected: "Element *" Received: {Symbol(async_id_symbol): 19758, Symbol(trigger_async_id_symbol): 10889, Symbol(kResourceStore): undefined} 16 | await page.goto("/"); 17 | await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); > 18 | expect(page.title()).toEqual("Element *"); | ^ 19 | }); 20 | test("Shows standard branding when showing a room", async ({ app, pageWithCredentials: page }) => { 21 | await app.client.createRoom({ name: "Test Room" }); at /home/runner/work/element-web/element-web/playwright/e2e/branding/title.spec.ts:18:30
});
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",
},
});
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");

Check failure on line 36 in playwright/e2e/branding/title.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › branding/title.spec.ts:33:9 › Test with custom branding › Shows custom branding when showing the home page

3) [Chrome] › branding/title.spec.ts:33:9 › Test with custom branding › Shows custom branding when showing the home page Error: expect(received).toEqual(expected) // deep equality Expected: "TestingApp TestBrand * $ignoredParameter" Received: {Symbol(async_id_symbol): 19577, Symbol(trigger_async_id_symbol): 10834, Symbol(kResourceStore): undefined} 34 | await page.goto("/"); 35 | await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); > 36 | expect(page.title()).toEqual("TestingApp TestBrand * $ignoredParameter"); | ^ 37 | }); 38 | test("Shows custom branding when showing a room", async ({ app, pageWithCredentials: page }) => { 39 | await app.client.createRoom({ name: "Test Room" }); at /home/runner/work/element-web/element-web/playwright/e2e/branding/title.spec.ts:36:30

Check failure on line 36 in playwright/e2e/branding/title.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › branding/title.spec.ts:33:9 › Test with custom branding › Shows custom branding when showing the home page

3) [Chrome] › branding/title.spec.ts:33:9 › Test with custom branding › Shows custom branding when showing the home page Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality Expected: "TestingApp TestBrand * $ignoredParameter" Received: {Symbol(async_id_symbol): 31468, Symbol(trigger_async_id_symbol): 15173, Symbol(kResourceStore): undefined} 34 | await page.goto("/"); 35 | await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); > 36 | expect(page.title()).toEqual("TestingApp TestBrand * $ignoredParameter"); | ^ 37 | }); 38 | test("Shows custom branding when showing a room", async ({ app, pageWithCredentials: page }) => { 39 | await app.client.createRoom({ name: "Test Room" }); at /home/runner/work/element-web/element-web/playwright/e2e/branding/title.spec.ts:36:30

Check failure on line 36 in playwright/e2e/branding/title.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 1/6

[Chrome] › branding/title.spec.ts:33:9 › Test with custom branding › Shows custom branding when showing the home page

3) [Chrome] › branding/title.spec.ts:33:9 › Test with custom branding › Shows custom branding when showing the home page Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toEqual(expected) // deep equality Expected: "TestingApp TestBrand * $ignoredParameter" Received: {Symbol(async_id_symbol): 19180, Symbol(trigger_async_id_symbol): 10768, Symbol(kResourceStore): undefined} 34 | await page.goto("/"); 35 | await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 }); > 36 | expect(page.title()).toEqual("TestingApp TestBrand * $ignoredParameter"); | ^ 37 | }); 38 | test("Shows custom branding when showing a room", async ({ app, pageWithCredentials: page }) => { 39 | await app.client.createRoom({ name: "Test Room" }); at /home/runner/work/element-web/element-web/playwright/e2e/branding/title.spec.ts:36:30
});
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");
});
});
72 changes: 50 additions & 22 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
import { LoginSplashView } from "./auth/LoginSplashView";
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";

Check failure on line 133 in src/components/structures/MatrixChat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

There should be at least one empty line between import groups
import { AppTitleContext } from "@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions";

Check failure on line 134 in src/components/structures/MatrixChat.tsx

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Cannot find module '@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions' or its corresponding type declarations.

Check failure on line 134 in src/components/structures/MatrixChat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

`@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions` import should occur before import of `../../PosthogTrackers`

// legacy export
export { default as Views } from "../../Views";
Expand Down Expand Up @@ -223,7 +224,6 @@
private tokenLogin?: boolean;
// What to focus on next component update, if anything
private focusNext: FocusNextType;
private subTitleStatus: string;
private prevWindowWidth: number;

private readonly loggedInView = createRef<LoggedInViewType>();
Expand All @@ -232,6 +232,8 @@
private fontWatcher?: FontWatcher;
private readonly stores: SdkContextClass;

private subtitleContext?: {unreadNotificationCount: number, userNotificationLevel: NotificationLevel, syncState: SyncState};

Check failure on line 235 in src/components/structures/MatrixChat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Expected a semicolon

Check failure on line 235 in src/components/structures/MatrixChat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Expected a semicolon

public constructor(props: IProps) {
super(props);
this.stores = SdkContextClass.instance;
Expand Down Expand Up @@ -275,10 +277,6 @@
}

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 = "";
}

/**
Expand Down Expand Up @@ -1474,7 +1472,7 @@
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = "";
this.subtitleContext = undefined;
this.setPageSubtitle();
this.stores.onLoggedOut();
}
Expand All @@ -1490,7 +1488,7 @@
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = "";
this.subtitleContext = undefined;
this.setPageSubtitle();
}

Expand Down Expand Up @@ -1941,15 +1939,51 @@
});
}

private setPageSubtitle(subtitle = ""): void {
private setPageSubtitle(): void {
const extraContext = this.subtitleContext;
let context: AppTitleContext = {
brand: SdkConfig.get().brand,
syncError: extraContext?.syncState === SyncState.Error,
notificationsMuted: extraContext && extraContext.userNotificationLevel < NotificationLevel.Activity,
unreadNotificationCount: extraContext?.unreadNotificationCount,
};

if (this.state.currentRoomId) {
const client = MatrixClientPeg.get();
const room = client?.getRoom(this.state.currentRoomId);
if (room) {
subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`;
context = {
...context,
roomId: this.state.currentRoomId,
roomName: room?.name,
};
}

const moduleTitle = ModuleRunner.instance.extensions.branding?.getAppTitle(context);
if (moduleTitle) {
if (document.title !== moduleTitle) {
document.title = moduleTitle;
}
return;
}

// Use application default.

let subtitle = "";
if (context?.syncError) {
subtitle += `[${_t("common|offline")}] `;
}
if (context.unreadNotificationCount !== undefined && context.unreadNotificationCount > 0) {
subtitle += `[${context.unreadNotificationCount}]`;
} else if (context.notificationsMuted !== undefined && !context.notificationsMuted) {
subtitle += `*`;
}

if ('roomId' in context && context.roomId) {
if (context.roomName) {
subtitle = `${subtitle} | ${context.roomName}`;
}
} else {
subtitle = `${this.subTitleStatus} ${subtitle}`;
subtitle = subtitle;

Check failure on line 1986 in src/components/structures/MatrixChat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'subtitle' is assigned to itself
}

const title = `${SdkConfig.get().brand} ${subtitle}`;
Expand All @@ -1966,17 +2000,11 @@
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();
};

Expand Down
24 changes: 24 additions & 0 deletions src/modules/ModuleRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
DefaultExperimentalExtensions,
ProvideExperimentalExtensions,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
import {
ProvideBrandingExtensions,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions";

Check failure on line 22 in src/modules/ModuleRunner.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Cannot find module '@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions' or its corresponding type declarations.


import { AppModule } from "./AppModule";
import { ModuleFactory } from "./ModuleFactory";
Expand All @@ -30,6 +34,7 @@
// 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;
Expand Down Expand Up @@ -67,6 +72,15 @@
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.
*
Expand Down Expand Up @@ -100,6 +114,16 @@
);
}
}

if (runtimeModule.extensions?.branding) {

Check failure on line 118 in src/modules/ModuleRunner.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'branding' does not exist on type 'AllExtensions'.
if (!this.brandingExtension) {
this.brandingExtension = runtimeModule.extensions?.branding;

Check failure on line 120 in src/modules/ModuleRunner.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'branding' does not exist on type 'AllExtensions'.
} else {
throw new Error(
`adding experimental branding implementation from module ${runtimeModule.moduleName} but an implementation was already provided.`,
);
}
}
}
}

Expand Down
Loading