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 #28979

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 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
14 changes: 14 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,20 @@ 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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue here that this (by omission) assures these values to be stable. Should we add a disclaimer that these parameters may change at any time?

Copy link
Member

@t3chguy t3chguy Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because our config flags are to be considered stable and changes should include backwards compatibility, its one of the reason config options tend to need product approval as its something we're committing to supporting for a good long time given we're not semver compliant it is much harder to spot "breaking" configuration changes

Like we still support camelCase & snake_case even though we switched to the latter multiple years ago. Like we still support default_hs_url even though we switched to well-knowns many years ago.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I'll put this under product review then. My understanding is the feature itself was greenlit, though I think we should be aware of the configuration implications.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aware of the configuration implications.

Maintenance implications*

It may want to be an Element Module to avoid that, or Product should derive the exact functions it should support so it can live & be supported mainline


- `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
Expand Down
46 changes: 46 additions & 0 deletions playwright/e2e/branding/title.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});

2 changes: 2 additions & 0 deletions src/IConfigOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Branding seems to consist of mostly urls, but it felt like a reasonable place to put this. Shout if this should be somewhere else.

title_template_in_room?: string;
};

force_verification?: boolean; // if true, users must verify new logins
Expand Down
31 changes: 25 additions & 6 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private subTitleStatus: string;
private prevWindowWidth: number;

private readonly titleTemplate: string;
private readonly titleTemplateInRoom: string;

private readonly loggedInView = createRef<LoggedInViewType>();
private dispatcherRef?: string;
private themeWatcher?: ThemeWatcher;
Expand Down Expand Up @@ -281,6 +284,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// 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';
}

/**
Expand Down Expand Up @@ -1106,6 +1112,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
this.setStateForNewView({
view: Views.WELCOME,
currentRoomId: null,
});
this.notifyNewScreen("welcome");
ThemeController.isLogin = true;
Expand All @@ -1115,6 +1122,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private viewLogin(otherState?: any): void {
this.setStateForNewView({
view: Views.LOGIN,
currentRoomId: null,
...otherState,
});
this.notifyNewScreen("login");
Expand Down Expand Up @@ -1949,21 +1957,32 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}

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(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ I wrote my own templating function here.

I feel like an app this size must be using templating elsewhere but couldn't find any examples or libraries I was particularly keen on, but pointers in this direction would be appreciated.

(title: string, [key, value]) => title.replaceAll(key, (value ?? '').replaceAll('$', '$_DLR$')), titleTemplate);

if (document.title !== title) {
document.title = title;
document.title = title.replaceAll('$_DLR$', '$');
}
}

Expand Down
Loading