Extend the Module API in prep of restricted-guests module

This commit is contained in:
Michael Telatynski 2025-06-23 09:32:26 +01:00
parent e481865aa6
commit 9763807c42
9 changed files with 342 additions and 6 deletions

View File

@ -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<void>;
}
// @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<M, P>(initialOptions: DialogOptions, dialog: ComponentType<P & DialogProps<M>>, props?: P): DialogHandle<M>;
}
// @public
export type DialogHandle<M> = {
finished: Promise<{
ok: boolean;
model: M;
}>;
close(): void;
};
// @public
export interface DialogOptions {
title: string;
}
// @public
export type DialogProps<M> = {
onSubmit(model: M): void;
cancel(): void;
};
// @alpha @deprecated (undocumented)
export interface DirectoryCustomisations {
// (undocumented)
@ -216,11 +269,28 @@ export class ModuleLoader {
start(): Promise<void>;
}
// @public
export interface NavigationApi {
toMatrixToLink(link: string, join?: boolean): Promise<void>;
}
// @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<Profile>;
}
// @alpha @deprecated (undocumented)
export interface RoomListCustomisations<Room> {
isRoomVisible?(room: Room): boolean;

View File

@ -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<void>;
}

View File

@ -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 <YourCustomRoomPreviewBarComponent {...props} />;
* }
* return <YourCustomComponent mxEvent={props.mxEvent} />;
* });
* ```
*/
registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void;
}

View File

@ -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<M> = {
/**
* 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<M> = {
/**
* 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<M, P>(
initialOptions: DialogOptions,
dialog: ComponentType<P & DialogProps<M>>,
props?: P,
): DialogHandle<M>;
}

View File

@ -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.

View File

@ -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<void>;
}

View File

@ -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<Profile>;
}

View File

@ -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<T> = (value: T) => void;
/**
* Utility class to wrap a value and allow listeners to be notified when the value changes.
*/
export class Watchable<T> {
private readonly listeners = new Set<WatchFn<T>>();
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);
}
}

View File

@ -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";