mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-15 02:00:24 +00:00
Compare commits
8 Commits
hs/better-
...
hs/customi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf9854a4c | ||
|
|
b589757c34 | ||
|
|
0d576b217b | ||
|
|
80cc0a928f | ||
|
|
9cccbeb799 | ||
|
|
a8c170f8be | ||
|
|
f5402b4ec4 | ||
|
|
131b28ede8 |
43
playwright/e2e/branding/title.spec.ts
Normal file
43
playwright/e2e/branding/title.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -131,6 +131,7 @@ import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"
|
|||||||
import { LoginSplashView } from "./auth/LoginSplashView";
|
import { LoginSplashView } from "./auth/LoginSplashView";
|
||||||
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
|
||||||
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";
|
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore";
|
||||||
|
import { AppTitleContext } from "@matrix-org/react-sdk-module-api/lib/lifecycles/BrandingExtensions";
|
||||||
|
|
||||||
// legacy export
|
// legacy export
|
||||||
export { default as Views } from "../../Views";
|
export { default as Views } from "../../Views";
|
||||||
@@ -223,7 +224,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
private tokenLogin?: boolean;
|
private tokenLogin?: boolean;
|
||||||
// What to focus on next component update, if anything
|
// What to focus on next component update, if anything
|
||||||
private focusNext: FocusNextType;
|
private focusNext: FocusNextType;
|
||||||
private subTitleStatus: string;
|
|
||||||
private prevWindowWidth: number;
|
private prevWindowWidth: number;
|
||||||
|
|
||||||
private readonly loggedInView = createRef<LoggedInViewType>();
|
private readonly loggedInView = createRef<LoggedInViewType>();
|
||||||
@@ -232,6 +232,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
private fontWatcher?: FontWatcher;
|
private fontWatcher?: FontWatcher;
|
||||||
private readonly stores: SdkContextClass;
|
private readonly stores: SdkContextClass;
|
||||||
|
|
||||||
|
private subtitleContext?: {unreadNotificationCount: number, userNotificationLevel: NotificationLevel, syncState: SyncState};
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.stores = SdkContextClass.instance;
|
this.stores = SdkContextClass.instance;
|
||||||
@@ -275,10 +277,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
|
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,
|
collapseLhs: false,
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
});
|
});
|
||||||
this.subTitleStatus = "";
|
this.subtitleContext = undefined;
|
||||||
this.setPageSubtitle();
|
this.setPageSubtitle();
|
||||||
this.stores.onLoggedOut();
|
this.stores.onLoggedOut();
|
||||||
}
|
}
|
||||||
@@ -1490,7 +1488,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
collapseLhs: false,
|
collapseLhs: false,
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
});
|
});
|
||||||
this.subTitleStatus = "";
|
this.subtitleContext = undefined;
|
||||||
this.setPageSubtitle();
|
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) {
|
if (this.state.currentRoomId) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client?.getRoom(this.state.currentRoomId);
|
const room = client?.getRoom(this.state.currentRoomId);
|
||||||
if (room) {
|
context = {
|
||||||
subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`;
|
...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 {
|
} else {
|
||||||
subtitle = `${this.subTitleStatus} ${subtitle}`;
|
subtitle = subtitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = `${SdkConfig.get().brand} ${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()!.setErrorStatus(state === SyncState.Error);
|
||||||
PlatformPeg.get()!.setNotificationCount(numUnreadRooms);
|
PlatformPeg.get()!.setNotificationCount(numUnreadRooms);
|
||||||
}
|
}
|
||||||
|
this.subtitleContext = {
|
||||||
this.subTitleStatus = "";
|
syncState: state,
|
||||||
if (state === SyncState.Error) {
|
userNotificationLevel: notificationState.level,
|
||||||
this.subTitleStatus += `[${_t("common|offline")}] `;
|
unreadNotificationCount: numUnreadRooms,
|
||||||
}
|
};
|
||||||
if (numUnreadRooms > 0) {
|
|
||||||
this.subTitleStatus += `[${numUnreadRooms}]`;
|
|
||||||
} else if (notificationState.level >= NotificationLevel.Activity) {
|
|
||||||
this.subTitleStatus += `*`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setPageSubtitle();
|
this.setPageSubtitle();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import {
|
|||||||
DefaultExperimentalExtensions,
|
DefaultExperimentalExtensions,
|
||||||
ProvideExperimentalExtensions,
|
ProvideExperimentalExtensions,
|
||||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/ExperimentalExtensions";
|
} 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 { AppModule } from "./AppModule";
|
||||||
import { ModuleFactory } from "./ModuleFactory";
|
import { ModuleFactory } from "./ModuleFactory";
|
||||||
@@ -30,6 +34,7 @@ class ExtensionsManager {
|
|||||||
// Private backing fields for extensions
|
// Private backing fields for extensions
|
||||||
private cryptoSetupExtension: ProvideCryptoSetupExtensions;
|
private cryptoSetupExtension: ProvideCryptoSetupExtensions;
|
||||||
private experimentalExtension: ProvideExperimentalExtensions;
|
private experimentalExtension: ProvideExperimentalExtensions;
|
||||||
|
private brandingExtension?: ProvideBrandingExtensions;
|
||||||
|
|
||||||
/** `true` if `cryptoSetupExtension` is the default implementation; `false` if it is implemented by a module. */
|
/** `true` if `cryptoSetupExtension` is the default implementation; `false` if it is implemented by a module. */
|
||||||
private hasDefaultCryptoSetupExtension = true;
|
private hasDefaultCryptoSetupExtension = true;
|
||||||
@@ -67,6 +72,15 @@ class ExtensionsManager {
|
|||||||
return this.experimentalExtension;
|
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.
|
* 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.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user