Rebrand to the "brand" API and add title support.

This commit is contained in:
Half-Shot 2025-06-23 13:08:07 +01:00
parent 3ffcaba20b
commit ced693c256
5 changed files with 106 additions and 62 deletions

View File

@ -141,6 +141,8 @@ import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenFor
import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload";
import Markdown from "../../Markdown";
import { sanitizeHtmlParams } from "../../Linkify";
import moduleApi from "../../modules/Api"
import { TitleRenderOptions } from "@element-hq/element-web-module-api";
// legacy export
export { default as Views } from "../../Views";
@ -227,7 +229,7 @@ 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 subTitleState: TitleRenderOptions;
private prevWindowWidth: number;
private readonly loggedInView = createRef<LoggedInViewType>();
@ -283,7 +285,7 @@ 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.subTitleState = {};
}
/**
@ -1505,7 +1507,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = "";
this.subTitleState = {};
this.setPageSubtitle();
this.stores.onLoggedOut();
}
@ -1521,7 +1523,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
collapseLhs: false,
currentRoomId: null,
});
this.subTitleStatus = "";
this.subTitleState = {};
this.setPageSubtitle();
}
@ -1991,19 +1993,43 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({ action: "message_sent" });
});
}
private setPageSubtitle(subtitle = ""): void {
private setPageSubtitle(): void {
let roomName: string|undefined;
if (this.state.currentRoomId) {
const client = MatrixClientPeg.get();
const room = client?.getRoom(this.state.currentRoomId);
if (room) {
subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`;
}
} else {
subtitle = `${this.subTitleStatus} ${subtitle}`;
roomName = client?.getRoom(this.state.currentRoomId)?.name;
}
let title = moduleApi.brandApi.renderTitle({...this.subTitleState, roomName, roomId: this.state.currentRoomId ?? undefined});
if (title === undefined) {
// No module API implemented, fallback
let subTitleStatus = "";
if (this.subTitleState.errorDidOccur) {
subTitleStatus += `[${_t("common|offline")}] `
}
if ((this.subTitleState.notificationCount ?? 0) > 0) {
subTitleStatus += `[${this.subTitleState.notificationCount}]`;
} else if (this.subTitleState.notificationsEnabled) {
subTitleStatus += `*`;
}
let subtitle;
if (this.state.currentRoomId) {
if (roomName) {
subtitle = `${subTitleStatus} | ${roomName} ${subtitle}`;
}
} else {
subtitle = `${subTitleStatus} ${subtitle}`;
}
title = `${SdkConfig.get().brand} ${subtitle}`;
}
const title = `${SdkConfig.get().brand} ${subtitle}`;
if (document.title !== title) {
document.title = title;
@ -2011,22 +2037,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
private onUpdateStatusIndicator = (notificationState: SummarizedNotificationState, state: SyncState): void => {
const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here
const notificationCount = notificationState.numUnreadStates; // we know that states === rooms here
if (PlatformPeg.get()) {
PlatformPeg.get()!.setErrorStatus(state === SyncState.Error);
PlatformPeg.get()!.setNotificationCount(numUnreadRooms);
PlatformPeg.get()!.setNotificationCount(notificationCount);
}
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.subTitleState = {
errorDidOccur: state === SyncState.Error,
notificationCount,
notificationsEnabled: notificationState.level >= NotificationLevel.Activity,
};
this.setPageSubtitle();
};

View File

@ -242,7 +242,7 @@ export default class Favicon {
return;
}
const badgeUrl = moduleApi.faviconApi.renderFavicon(opts) || this.renderBadge(opts);
const badgeUrl = moduleApi.brandApi.renderFavicon(opts) || this.renderBadge(opts);
this.setIcon(badgeUrl);
}

View File

@ -22,7 +22,7 @@ import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.
import { ConfigApi } from "./ConfigApi.ts";
import { I18nApi } from "./I18nApi.ts";
import { CustomComponentsApi } from "./customComponentApi.ts";
import { FaviconApi } from "./faviconApi.ts";
import { BrandApi } from "./brandApi.ts";
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
let used = false;
@ -62,7 +62,7 @@ class ModuleApi implements Api {
public readonly i18n = new I18nApi();
public readonly customComponents = new CustomComponentsApi();
public readonly rootNode = document.getElementById("matrixchat")!;
public readonly faviconApi = new FaviconApi();
public readonly brandApi = new BrandApi();
public createRoot(element: Element): Root {
return createRoot(element);

58
src/modules/brandApi.ts Normal file
View File

@ -0,0 +1,58 @@
/*
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 type {
BrandApi as IBrandApi,
FaviconRenderFunction,
FaviconRenderOptions,
TitleRenderFunction,
TitleRenderOptions
} from "@element-hq/element-web-module-api";
export class BrandApi implements IBrandApi {
private registeredFaviconFunction?: FaviconRenderFunction;
private registeredTitleFunction?: TitleRenderFunction;
public registerFaviconRenderer(
func: FaviconRenderFunction
): void {
if (this.registeredFaviconFunction) {
throw Error('A custom favicon rendering function has already been registered');
}
this.registeredFaviconFunction = func;
}
public registerTitleRenderer(
func: TitleRenderFunction
): void {
if (this.registeredTitleFunction) {
throw Error('A custom title rendering function has already been registered');
}
this.registeredTitleFunction = func;
}
/**
* Returns a URL to a rendered favicon if a module has generated one, otherwise
* this returns undefined.
* @param opts Options to pass to the render function.
* @returns A URL string, or undefined.
*/
public renderFavicon(opts: FaviconRenderOptions): string|undefined {
return this.registeredFaviconFunction?.(opts);
}
/**
* Returns the title text if a module has generated one, otherwise
* this returns undefined.
* @param opts Options to pass to the render function.
* @returns Title text, or undefined.
*/
public renderTitle(opts: TitleRenderOptions): string|undefined {
return this.registeredTitleFunction?.(opts);
}
}

View File

@ -1,36 +0,0 @@
/*
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 type {
FaviconApi as IFaviconApi,
FaviconRenderFunction,
FaviconRenderOptions
} from "@element-hq/element-web-module-api";
export class FaviconApi implements IFaviconApi {
private registeredFunction?: FaviconRenderFunction;
public registerRenderer(
func: FaviconRenderFunction
): void {
if (this.registeredFunction) {
throw Error('A custom favicon rendering function has already been registered');
}
this.registeredFunction = func;
}
/**
* Returns a URL to a rendered favicon if a module has generated one, otherwise
* this returns undefined.
* @param opts Options to pass to the render function.
* @returns A URL string, or undefined.
*/
public renderFavicon(opts: FaviconRenderOptions): string|undefined {
return this.registeredFunction?.(opts);
}
}