Compare commits

...

8 Commits

Author SHA1 Message Date
Half-Shot
bcf9854a4c Fix so that we still use notificationsMuted / unread even if a room isn't in view. 2025-01-17 12:14:46 +00:00
Half-Shot
b589757c34 Replace with module API. 2025-01-17 11:48:53 +00:00
Half-Shot
0d576b217b lint 2025-01-17 11:48:53 +00:00
Half-Shot
80cc0a928f dollars 2025-01-17 11:48:53 +00:00
Half-Shot
9cccbeb799 clear current room in two more contexts. 2025-01-17 11:48:53 +00:00
Half-Shot
a8c170f8be Add tests 2025-01-17 11:48:53 +00:00
Half-Shot
f5402b4ec4 Add ability to customize the title template in branding. 2025-01-17 11:48:53 +00:00
Half-Shot
131b28ede8 New config options. 2025-01-17 11:48:53 +00:00
3 changed files with 117 additions and 22 deletions

View File

@@ -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 *");
});
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");
});
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");
});
});

View File

@@ -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,7 +224,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
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>();
@@ -232,6 +232,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
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;
@@ -275,10 +277,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
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 = "";
}
/**
@@ -1474,7 +1472,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = "";
this.subtitleContext = undefined;
this.setPageSubtitle();
this.stores.onLoggedOut();
}
@@ -1490,7 +1488,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = "";
this.subtitleContext = undefined;
this.setPageSubtitle();
}
@@ -1941,15 +1939,51 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
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;
}
const title = `${SdkConfig.get().brand} ${subtitle}`;
@@ -1966,17 +2000,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
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();
};

View File

@@ -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.`,
);
}
}
}
}