From 9763807c42e1ca386c9504d776c7b24f14458297 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 23 Jun 2025 09:32:26 +0100 Subject: [PATCH] Extend the Module API in prep of restricted-guests module --- .../element-web-module-api.api.md | 76 ++++++++++++++++++- .../element-web-module-api/src/api/auth.ts | 45 +++++++++++ .../src/api/custom-components.ts | 46 ++++++++++- .../element-web-module-api/src/api/dialog.ts | 68 +++++++++++++++++ .../element-web-module-api/src/api/index.ts | 18 ++++- .../src/api/navigation.ts | 19 +++++ .../element-web-module-api/src/api/profile.ts | 34 +++++++++ .../src/api/watchable.ts | 38 ++++++++++ packages/element-web-module-api/src/index.ts | 4 + 9 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 packages/element-web-module-api/src/api/auth.ts create mode 100644 packages/element-web-module-api/src/api/dialog.ts create mode 100644 packages/element-web-module-api/src/api/navigation.ts create mode 100644 packages/element-web-module-api/src/api/profile.ts create mode 100644 packages/element-web-module-api/src/api/watchable.ts diff --git a/packages/element-web-module-api/element-web-module-api.api.md b/packages/element-web-module-api/element-web-module-api.api.md index 6d6403735e..d0df58efb2 100644 --- a/packages/element-web-module-api/element-web-module-api.api.md +++ b/packages/element-web-module-api/element-web-module-api.api.md @@ -4,11 +4,26 @@ ```ts +import { ComponentType } from 'react'; import { JSX } from 'react'; import { ModuleApi } from '@matrix-org/react-sdk-module-api'; import { Root } from 'react-dom/client'; import { RuntimeModule } from '@matrix-org/react-sdk-module-api'; +// @public +export interface AccountAuthApiExtension { + overwriteAccountAuth(accountInfo: AccountAuthInfo): Promise; +} + +// @public +export interface AccountAuthInfo { + accessToken: string; + deviceId: string; + homeserverUrl: string; + refreshToken?: string; + userId: string; +} + // @alpha @deprecated (undocumented) export interface AliasCustomisations { // (undocumented) @@ -19,12 +34,13 @@ export interface AliasCustomisations { // Warning: (ae-incompatible-release-tags) The symbol "Api" is marked as @public, but its signature references "LegacyCustomisationsApiExtension" which is marked as @alpha // // @public -export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension { +export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension, DialogApiExtension, AccountAuthApiExtension, ProfileApiExtension { readonly config: ConfigApi; createRoot(element: Element): Root; // @alpha readonly customComponents: CustomComponentsApi; readonly i18n: I18nApi; + readonly navigation: NavigationApi; readonly rootNode: HTMLElement; } @@ -63,6 +79,7 @@ export interface ConfigApi { // @alpha export interface CustomComponentsApi { registerMessageRenderer(eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints?: CustomMessageRenderHints): void; + registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void; } // @alpha @@ -73,13 +90,49 @@ export type CustomMessageComponentProps = { // @alpha export type CustomMessageRenderFunction = ( props: CustomMessageComponentProps, -originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element) => JSX.Element; +originalComponent?: (props?: OriginalMessageComponentProps) => React.JSX.Element) => JSX.Element; // @alpha export type CustomMessageRenderHints = { allowEditingEvent?: boolean; }; +// @alpha +export type CustomRoomPreviewBarComponentProps = { + roomId?: string; + roomAlias?: string; +}; + +// @alpha +export type CustomRoomPreviewBarRenderFunction = ( +props: CustomRoomPreviewBarComponentProps, +originalComponent: (props: CustomRoomPreviewBarComponentProps) => JSX.Element) => JSX.Element; + +// @public +export interface DialogApiExtension { + openDialog(initialOptions: DialogOptions, dialog: ComponentType

>, props?: P): DialogHandle; +} + +// @public +export type DialogHandle = { + finished: Promise<{ + ok: boolean; + model: M; + }>; + close(): void; +}; + +// @public +export interface DialogOptions { + title: string; +} + +// @public +export type DialogProps = { + onSubmit(model: M): void; + cancel(): void; +}; + // @alpha @deprecated (undocumented) export interface DirectoryCustomisations { // (undocumented) @@ -216,11 +269,28 @@ export class ModuleLoader { start(): Promise; } +// @public +export interface NavigationApi { + toMatrixToLink(link: string, join?: boolean): Promise; +} + // @alpha -export type OriginalComponentProps = { +export type OriginalMessageComponentProps = { showUrlPreview?: boolean; }; +// @public +export interface Profile { + displayName?: string; + userId?: string; +} + +// @public +export interface ProfileApiExtension { + // Warning: (ae-forgotten-export) The symbol "Watchable" needs to be exported by the entry point index.d.ts + readonly profile: Watchable; +} + // @alpha @deprecated (undocumented) export interface RoomListCustomisations { isRoomVisible?(room: Room): boolean; diff --git a/packages/element-web-module-api/src/api/auth.ts b/packages/element-web-module-api/src/api/auth.ts new file mode 100644 index 0000000000..5ecdcd3c25 --- /dev/null +++ b/packages/element-web-module-api/src/api/auth.ts @@ -0,0 +1,45 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +/** + * Interface for account authentication information, used for overwriting the current account's authentication state. + * @public + */ +export interface AccountAuthInfo { + /** + * The user ID. + */ + userId: string; + /** + * The device ID. + */ + deviceId: string; + /** + * The access token belonging to this device ID and user ID. + */ + accessToken: string; + /** + * The refresh token belonging to this device ID and user ID. + */ + refreshToken?: string; + /** + * The homeserver URL where the credentials are valid. + */ + homeserverUrl: string; +} + +/** + * Methods to manage authentication in the application. + * @public + */ +export interface AccountAuthApiExtension { + /** + * Overwrite the current account's authentication state with the provided account information. + * @param accountInfo - The account authentication information to overwrite the current state with. + */ + overwriteAccountAuth(accountInfo: AccountAuthInfo): Promise; +} diff --git a/packages/element-web-module-api/src/api/custom-components.ts b/packages/element-web-module-api/src/api/custom-components.ts index becc0f1992..b97728dc05 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -24,7 +24,7 @@ export type CustomMessageComponentProps = { * Properties to alter the render function of the original component. * @alpha Subject to change. */ -export type OriginalComponentProps = { +export type OriginalMessageComponentProps = { /** * Should previews be shown for this event. * This may be overriden by user preferences. @@ -58,7 +58,31 @@ export type CustomMessageRenderFunction = ( /** * Render function for the original component. This may be omitted if the message would not normally be rendered. */ - originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element, + originalComponent?: (props?: OriginalMessageComponentProps) => React.JSX.Element, +) => JSX.Element; + +/** + * Properties for all message components. + * @alpha Subject to change. + */ +export type CustomRoomPreviewBarComponentProps = { + roomId?: string; + roomAlias?: string; +}; + +/** + * Function used to render a room preview bar component. + * @alpha Unlikely to change + */ +export type CustomRoomPreviewBarRenderFunction = ( + /** + * Properties for the room preview bar to be rendered. + */ + props: CustomRoomPreviewBarComponentProps, + /** + * Render function for the original component. + */ + originalComponent: (props: CustomRoomPreviewBarComponentProps) => JSX.Element, ) => JSX.Element; /** @@ -95,4 +119,22 @@ export interface CustomComponentsApi { renderer: CustomMessageRenderFunction, hints?: CustomMessageRenderHints, ): void; + + /** + * Register a renderer for the room preview bar. + * + * The render function should return a rendered component. + * + * @param renderer - The render function for the room preview bar. + * @example + * ``` + * customComponents.registerRoomPreviewBar((props, OriginalComponent) => { + * if (props.roomId === "!some_special_room_id:server") { + * return ; + * } + * return ; + * }); + * ``` + */ + registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void; } diff --git a/packages/element-web-module-api/src/api/dialog.ts b/packages/element-web-module-api/src/api/dialog.ts new file mode 100644 index 0000000000..a11a25d432 --- /dev/null +++ b/packages/element-web-module-api/src/api/dialog.ts @@ -0,0 +1,68 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { ComponentType } from "react"; + +/** + * Options for {@link Api#openDialog}. + * @public + */ +export interface DialogOptions { + /** + * The title of the dialog. + */ + title: string; +} + +/** + * Handle returned by {@link Api#openDialog}. + * @public + */ +export type DialogHandle = { + /** + * Promise that resolves when the dialog is finished. + */ + finished: Promise<{ ok: boolean; model: M }>; + /** + * Method to close the dialog. + */ + close(): void; +}; + +/** + * Props passed to the dialog body component. + * @public + */ +export type DialogProps = { + /** + * Callback to submit the dialog. + * @param model - The model to submit with the dialog. This is typically the data collected. + */ + onSubmit(model: M): void; + /** + * Cancel the dialog programmatically. + */ + cancel(): void; +}; + +/** + * Methods to manage dialogs in the application. + * @public + */ +export interface DialogApiExtension { + /** + * Open a dialog with the given options and body component and return a handle to it. + * @param initialOptions - The initial options for the dialog, such as title and action label. + * @param dialog - The body component to render in the dialog. This component should accept props of type `P`. + * @param props - Additional props to pass to the body + */ + openDialog( + initialOptions: DialogOptions, + dialog: ComponentType

>, + props?: P, + ): DialogHandle; +} diff --git a/packages/element-web-module-api/src/api/index.ts b/packages/element-web-module-api/src/api/index.ts index 399d807a14..8a59260773 100644 --- a/packages/element-web-module-api/src/api/index.ts +++ b/packages/element-web-module-api/src/api/index.ts @@ -11,6 +11,10 @@ import { LegacyCustomisationsApiExtension } from "./legacy-customisations"; import { ConfigApi } from "./config"; import { I18nApi } from "./i18n"; import { CustomComponentsApi } from "./custom-components"; +import { NavigationApi } from "./navigation.ts"; +import { DialogApiExtension } from "./dialog.ts"; +import { AccountAuthApiExtension } from "./auth.ts"; +import { ProfileApiExtension } from "./profile.ts"; /** * Module interface for modules to implement. @@ -69,7 +73,12 @@ export function isModule(module: unknown): module is ModuleExport { * The API for modules to interact with the application. * @public */ -export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension { +export interface Api + extends LegacyModuleApiExtension, + LegacyCustomisationsApiExtension, + DialogApiExtension, + AccountAuthApiExtension, + ProfileApiExtension { /** * The API to read config.json values. * Keys should be scoped to the module in reverse domain name notation. @@ -93,6 +102,13 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx * @alpha */ readonly customComponents: CustomComponentsApi; + + /** + * API to navigate the application. + * @public + */ + readonly navigation: NavigationApi; + /** * Create a ReactDOM root for rendering React components. * Exposed to allow modules to avoid needing to bundle their own ReactDOM. diff --git a/packages/element-web-module-api/src/api/navigation.ts b/packages/element-web-module-api/src/api/navigation.ts new file mode 100644 index 0000000000..a3b073274c --- /dev/null +++ b/packages/element-web-module-api/src/api/navigation.ts @@ -0,0 +1,19 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +/** + * API methods to navigate the application. + * @public + */ +export interface NavigationApi { + /** + * Navigate to a permalink, optionally causing a join if the user is not already a member of the room/space. + * @param link - The permalink to navigate to, e.g. `https://matrix.to/#/!roomId:example.com`. + * @param join - If true, the user will be made to attempt to join the room/space if they are not already a member. + */ + toMatrixToLink(link: string, join?: boolean): Promise; +} diff --git a/packages/element-web-module-api/src/api/profile.ts b/packages/element-web-module-api/src/api/profile.ts new file mode 100644 index 0000000000..097a28d86f --- /dev/null +++ b/packages/element-web-module-api/src/api/profile.ts @@ -0,0 +1,34 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { Watchable } from "./watchable.ts"; + +/** + * The profile of the user currently logged in. + * @public + */ +export interface Profile { + /** + * The user ID of the logged-in user, if undefined then no user is logged in. + */ + userId?: string; + /** + * The display name of the logged-in user. + */ + displayName?: string; +} + +/** + * API extensions for modules to access the profile of the logged-in user. + * @public + */ +export interface ProfileApiExtension { + /** + * The profile of the user currently logged in. + */ + readonly profile: Watchable; +} diff --git a/packages/element-web-module-api/src/api/watchable.ts b/packages/element-web-module-api/src/api/watchable.ts new file mode 100644 index 0000000000..7f74f775c1 --- /dev/null +++ b/packages/element-web-module-api/src/api/watchable.ts @@ -0,0 +1,38 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +type WatchFn = (value: T) => void; + +/** + * Utility class to wrap a value and allow listeners to be notified when the value changes. + */ +export class Watchable { + private readonly listeners = new Set>(); + + public constructor(private currentValue: T) {} + + public get value(): T { + return this.currentValue; + } + + public set value(value: T) { + if (this.currentValue !== value) { + this.currentValue = value; + for (const listener of this.listeners) { + listener(this.currentValue); + } + } + } + + public watch(listener: (value: T) => void): void { + this.listeners.add(listener); + } + + public unwatch(listener: (value: T) => void): void { + this.listeners.delete(listener); + } +} diff --git a/packages/element-web-module-api/src/index.ts b/packages/element-web-module-api/src/index.ts index 0a7337ec21..952480135f 100644 --- a/packages/element-web-module-api/src/index.ts +++ b/packages/element-web-module-api/src/index.ts @@ -13,3 +13,7 @@ export type * from "./models/event"; export type * from "./api/custom-components"; export type * from "./api/legacy-modules"; export type * from "./api/legacy-customisations"; +export type * from "./api/auth"; +export type * from "./api/dialog"; +export type * from "./api/profile"; +export type * from "./api/navigation";