From c9ea5f8f4ffca79d1a180d80e2ce7cafbc443d76 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 16 May 2025 15:49:07 +0100 Subject: [PATCH 01/14] Custom component concept piece --- .../element-web-module-api.api.md | 491 +++++++++--------- .../src/api/custom-components.ts | 86 +++ .../element-web-module-api/src/api/index.ts | 3 + packages/element-web-module-api/src/index.ts | 2 + 4 files changed, 349 insertions(+), 233 deletions(-) create mode 100644 packages/element-web-module-api/src/api/custom-components.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 a4b9456cb6..1e41a6da51 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 @@ -1,233 +1,258 @@ -## API Report File for "@element-hq/element-web-module-api" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { ModuleApi } from '@matrix-org/react-sdk-module-api'; -import { Root } from 'react-dom/client'; -import { RuntimeModule } from '@matrix-org/react-sdk-module-api'; - -// @alpha @deprecated (undocumented) -export interface AliasCustomisations { - // (undocumented) - getDisplayAliasForAliasSet?(canonicalAlias: string | null, altAliases: string[]): string | null; -} - -// Warning: (ae-incompatible-release-tags) The symbol "Api" is marked as @public, but its signature references "LegacyModuleApiExtension" which is marked as @alpha -// 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 { - readonly config: ConfigApi; - createRoot(element: Element): Root; - readonly i18n: I18nApi; - readonly rootNode: HTMLElement; -} - -// @alpha @deprecated (undocumented) -export interface ChatExportCustomisations { - getForceChatExportParameters(): { - format?: ExportFormat; - range?: ExportType; - numberOfMessages?: number; - includeAttachments?: boolean; - sizeMb?: number; - }; -} - -// @alpha @deprecated (undocumented) -export interface ComponentVisibilityCustomisations { - shouldShowComponent?(component: "UIComponent.sendInvites" | "UIComponent.roomCreation" | "UIComponent.spaceCreation" | "UIComponent.exploreRooms" | "UIComponent.addIntegrations" | "UIComponent.filterContainer" | "UIComponent.roomOptionsMenu"): boolean; -} - -// @public -export interface Config { - // (undocumented) - brand: string; -} - -// @public -export interface ConfigApi { - // (undocumented) - get(): Config; - // (undocumented) - get(key: K): Config[K]; - // (undocumented) - get(key?: K): Config | Config[K]; -} - -// @alpha @deprecated (undocumented) -export interface DirectoryCustomisations { - // (undocumented) - requireCanonicalAliasAccessToPublish?(): boolean; -} - -// @public -export interface I18nApi { - get language(): string; - register(translations: Partial): void; - translate(key: keyof Translations, variables?: Variables): string; -} - -// @alpha @deprecated (undocumented) -export type LegacyCustomisations = (customisations: T) => void; - -// @alpha @deprecated (undocumented) -export interface LegacyCustomisationsApiExtension { - // @deprecated (undocumented) - readonly _registerLegacyAliasCustomisations: LegacyCustomisations; - // @deprecated (undocumented) - readonly _registerLegacyChatExportCustomisations: LegacyCustomisations>; - // @deprecated (undocumented) - readonly _registerLegacyComponentVisibilityCustomisations: LegacyCustomisations; - // @deprecated (undocumented) - readonly _registerLegacyDirectoryCustomisations: LegacyCustomisations; - // @deprecated (undocumented) - readonly _registerLegacyLifecycleCustomisations: LegacyCustomisations; - // @deprecated (undocumented) - readonly _registerLegacyMediaCustomisations: LegacyCustomisations>; - // @deprecated (undocumented) - readonly _registerLegacyRoomListCustomisations: LegacyCustomisations>; - // @deprecated (undocumented) - readonly _registerLegacyUserIdentifierCustomisations: LegacyCustomisations; - // @deprecated (undocumented) - readonly _registerLegacyWidgetPermissionsCustomisations: LegacyCustomisations>; - // @deprecated (undocumented) - readonly _registerLegacyWidgetVariablesCustomisations: LegacyCustomisations; -} - -// @alpha @deprecated (undocumented) -export interface LegacyModuleApiExtension { - // @deprecated - _registerLegacyModule(LegacyModule: RuntimeModuleConstructor): Promise; -} - -// @alpha @deprecated (undocumented) -export interface LifecycleCustomisations { - // (undocumented) - onLoggedOutAndStorageCleared?(): void; -} - -// @alpha @deprecated (undocumented) -export interface Media { - // (undocumented) - downloadSource(): Promise; - // (undocumented) - getSquareThumbnailHttp(dim: number): string | null; - // (undocumented) - getThumbnailHttp(width: number, height: number, mode?: "scale" | "crop"): string | null; - // (undocumented) - getThumbnailOfSourceHttp(width: number, height: number, mode?: "scale" | "crop"): string | null; - // (undocumented) - readonly hasThumbnail: boolean; - // (undocumented) - readonly isEncrypted: boolean; - // (undocumented) - readonly srcHttp: string | null; - // (undocumented) - readonly srcMxc: string; - // (undocumented) - readonly thumbnailHttp: string | null; - // (undocumented) - readonly thumbnailMxc: string | null | undefined; -} - -// @alpha @deprecated (undocumented) -export interface MediaContructable { - // (undocumented) - new (prepared: PreparedMedia): Media; -} - -// @alpha @deprecated (undocumented) -export interface MediaCustomisations { - // (undocumented) - readonly Media: MediaContructable; - // (undocumented) - mediaFromContent(content: Content, client?: Client): Media; - // (undocumented) - mediaFromMxc(mxc?: string, client?: Client): Media; -} - -// @public -export interface Module { - // (undocumented) - load(): Promise; -} - -// @public -export interface ModuleFactory { - // (undocumented) - new (api: Api): Module; - // (undocumented) - readonly moduleApiVersion: string; - // (undocumented) - readonly prototype: Module; -} - -// @public -export class ModuleIncompatibleError extends Error { - constructor(pluginVersion: string); -} - -// @public -export class ModuleLoader { - constructor(api: Api); - // Warning: (ae-forgotten-export) The symbol "ModuleExport" needs to be exported by the entry point index.d.ts - // - // (undocumented) - load(moduleExport: ModuleExport): Promise; - // (undocumented) - start(): Promise; -} - -// @alpha @deprecated (undocumented) -export interface RoomListCustomisations { - isRoomVisible?(room: Room): boolean; -} - -// @alpha @deprecated (undocumented) -export type RuntimeModuleConstructor = new (api: ModuleApi) => RuntimeModule; - -// @public -export type Translations = Record; - -// @alpha @deprecated (undocumented) -export interface UserIdentifierCustomisations { - getDisplayUserIdentifier(userId: string, opts: { - roomId?: string; - withDisplayName?: boolean; - }): string | null; -} - -// @public -export type Variables = { - count?: number; - [key: string]: number | string | undefined; -}; - -// @alpha @deprecated (undocumented) -export interface WidgetPermissionsCustomisations { - preapproveCapabilities?(widget: Widget, requestedCapabilities: Set): Promise>; -} - -// @alpha @deprecated (undocumented) -export interface WidgetVariablesCustomisations { - isReady?(): Promise; - provideVariables?(): { - currentUserId: string; - userDisplayName?: string; - userHttpAvatarUrl?: string; - clientId?: string; - clientTheme?: string; - clientLanguage?: string; - deviceId?: string; - baseUrl?: string; - }; -} - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@element-hq/element-web-module-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +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'; + +// @alpha @deprecated (undocumented) +export interface AliasCustomisations { + // (undocumented) + getDisplayAliasForAliasSet?(canonicalAlias: string | null, altAliases: string[]): string | null; +} + +// Warning: (ae-incompatible-release-tags) The symbol "Api" is marked as @public, but its signature references "LegacyModuleApiExtension" which is marked as @alpha +// 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 { + readonly config: ConfigApi; + createRoot(element: Element): Root; + // (undocumented) + readonly customComponents: CustomComponentsApi; + readonly i18n: I18nApi; + readonly rootNode: HTMLElement; +} + +// @alpha @deprecated (undocumented) +export interface ChatExportCustomisations { + getForceChatExportParameters(): { + format?: ExportFormat; + range?: ExportType; + numberOfMessages?: number; + includeAttachments?: boolean; + sizeMb?: number; + }; +} + +// @alpha @deprecated (undocumented) +export interface ComponentVisibilityCustomisations { + shouldShowComponent?(component: "UIComponent.sendInvites" | "UIComponent.roomCreation" | "UIComponent.spaceCreation" | "UIComponent.exploreRooms" | "UIComponent.addIntegrations" | "UIComponent.filterContainer" | "UIComponent.roomOptionsMenu"): boolean; +} + +// @public +export interface Config { + // (undocumented) + brand: string; +} + +// @public +export interface ConfigApi { + // (undocumented) + get(): Config; + // (undocumented) + get(key: K): Config[K]; + // (undocumented) + get(key?: K): Config | Config[K]; +} + +// @public +export type CustomComponentProps = { + [CustomComponentTarget.TextualEvent]: { + mxEvent: any; + stripReply: boolean; + }; +}; + +// @public +export type CustomComponentRenderFunction = (props: CustomComponentProps[T], originalComponent: JSX.Element) => JSX.Element | null; + +// @public +export interface CustomComponentsApi { + register(target: T, renderer: CustomComponentRenderFunction): void; +} + +// @public +export enum CustomComponentTarget { + // (undocumented) + TextualEvent = "TextualEvent" +} + +// @alpha @deprecated (undocumented) +export interface DirectoryCustomisations { + // (undocumented) + requireCanonicalAliasAccessToPublish?(): boolean; +} + +// @public +export interface I18nApi { + get language(): string; + register(translations: Partial): void; + translate(key: keyof Translations, variables?: Variables): string; +} + +// @alpha @deprecated (undocumented) +export type LegacyCustomisations = (customisations: T) => void; + +// @alpha @deprecated (undocumented) +export interface LegacyCustomisationsApiExtension { + // @deprecated (undocumented) + readonly _registerLegacyAliasCustomisations: LegacyCustomisations; + // @deprecated (undocumented) + readonly _registerLegacyChatExportCustomisations: LegacyCustomisations>; + // @deprecated (undocumented) + readonly _registerLegacyComponentVisibilityCustomisations: LegacyCustomisations; + // @deprecated (undocumented) + readonly _registerLegacyDirectoryCustomisations: LegacyCustomisations; + // @deprecated (undocumented) + readonly _registerLegacyLifecycleCustomisations: LegacyCustomisations; + // @deprecated (undocumented) + readonly _registerLegacyMediaCustomisations: LegacyCustomisations>; + // @deprecated (undocumented) + readonly _registerLegacyRoomListCustomisations: LegacyCustomisations>; + // @deprecated (undocumented) + readonly _registerLegacyUserIdentifierCustomisations: LegacyCustomisations; + // @deprecated (undocumented) + readonly _registerLegacyWidgetPermissionsCustomisations: LegacyCustomisations>; + // @deprecated (undocumented) + readonly _registerLegacyWidgetVariablesCustomisations: LegacyCustomisations; +} + +// @alpha @deprecated (undocumented) +export interface LegacyModuleApiExtension { + // @deprecated + _registerLegacyModule(LegacyModule: RuntimeModuleConstructor): Promise; +} + +// @alpha @deprecated (undocumented) +export interface LifecycleCustomisations { + // (undocumented) + onLoggedOutAndStorageCleared?(): void; +} + +// @alpha @deprecated (undocumented) +export interface Media { + // (undocumented) + downloadSource(): Promise; + // (undocumented) + getSquareThumbnailHttp(dim: number): string | null; + // (undocumented) + getThumbnailHttp(width: number, height: number, mode?: "scale" | "crop"): string | null; + // (undocumented) + getThumbnailOfSourceHttp(width: number, height: number, mode?: "scale" | "crop"): string | null; + // (undocumented) + readonly hasThumbnail: boolean; + // (undocumented) + readonly isEncrypted: boolean; + // (undocumented) + readonly srcHttp: string | null; + // (undocumented) + readonly srcMxc: string; + // (undocumented) + readonly thumbnailHttp: string | null; + // (undocumented) + readonly thumbnailMxc: string | null | undefined; +} + +// @alpha @deprecated (undocumented) +export interface MediaContructable { + // (undocumented) + new (prepared: PreparedMedia): Media; +} + +// @alpha @deprecated (undocumented) +export interface MediaCustomisations { + // (undocumented) + readonly Media: MediaContructable; + // (undocumented) + mediaFromContent(content: Content, client?: Client): Media; + // (undocumented) + mediaFromMxc(mxc?: string, client?: Client): Media; +} + +// @public +export interface Module { + // (undocumented) + load(): Promise; +} + +// @public +export interface ModuleFactory { + // (undocumented) + new (api: Api): Module; + // (undocumented) + readonly moduleApiVersion: string; + // (undocumented) + readonly prototype: Module; +} + +// @public +export class ModuleIncompatibleError extends Error { + constructor(pluginVersion: string); +} + +// @public +export class ModuleLoader { + constructor(api: Api); + // Warning: (ae-forgotten-export) The symbol "ModuleExport" needs to be exported by the entry point index.d.ts + // + // (undocumented) + load(moduleExport: ModuleExport): Promise; + // (undocumented) + start(): Promise; +} + +// @alpha @deprecated (undocumented) +export interface RoomListCustomisations { + isRoomVisible?(room: Room): boolean; +} + +// @alpha @deprecated (undocumented) +export type RuntimeModuleConstructor = new (api: ModuleApi) => RuntimeModule; + +// @public +export type Translations = Record; + +// @alpha @deprecated (undocumented) +export interface UserIdentifierCustomisations { + getDisplayUserIdentifier(userId: string, opts: { + roomId?: string; + withDisplayName?: boolean; + }): string | null; +} + +// @public +export type Variables = { + count?: number; + [key: string]: number | string | undefined; +}; + +// @alpha @deprecated (undocumented) +export interface WidgetPermissionsCustomisations { + preapproveCapabilities?(widget: Widget, requestedCapabilities: Set): Promise>; +} + +// @alpha @deprecated (undocumented) +export interface WidgetVariablesCustomisations { + isReady?(): Promise; + provideVariables?(): { + currentUserId: string; + userDisplayName?: string; + userHttpAvatarUrl?: string; + clientId?: string; + clientTheme?: string; + clientLanguage?: string; + deviceId?: string; + baseUrl?: string; + }; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/element-web-module-api/src/api/custom-components.ts b/packages/element-web-module-api/src/api/custom-components.ts new file mode 100644 index 0000000000..582a281ce1 --- /dev/null +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -0,0 +1,86 @@ +import { ComponentType, JSX, MouseEventHandler, PropsWithChildren } from "react"; + +/** + * Target of the renderer function. + * @public + */ +export enum CustomComponentTarget { + /** + * TODO: We should make this more descriptive. + * The renderer for text events in the timeline, such as "m.room.message" + */ + TextualBody = "TextualBody", + MessageContextMenu = "MessageContextMenu", +} + +export interface ContextMenuItem { + label: string; + iconClassName: string; + onClick: ( + e: React.MouseEvent | React.KeyboardEvent | React.FormEvent, + ) => void | Promise; +} + +/** + * Properties for the render component. + * @public + */ +export type CustomComponentProps = { + [CustomComponentTarget.TextualBody]: { + /** + * The Matrix event information. + * TODO: Should this just use the types from matrix-js-sdk? + */ + mxEvent: any; + /** + * Words to highlight on + */ + highlights?: string[]; + /** + * Should previews be shown for this event + */ + showUrlPreview?: boolean; + /** + * Is this event being rendered to a static export + */ + forExport?: boolean; + }; + [CustomComponentTarget.MessageContextMenu]: { + /** + * The Matrix event information. + * TODO: Should this just use the types from matrix-js-sdk? + */ + mxEvent: any; + /** + * Close the menu + */ + closeMenu: () => void; + }; +}; +/** + * Render function. Returning null skips this function and passes it onto the next registered renderer. + * @public + */ +export type CustomComponentRenderFunction = ( + props: CustomComponentProps[T], + originalComponent: JSX.Element, +) => JSX.Element | null; + +/** + * The API for creating custom components. + * @public + */ +export interface CustomComponentsApi { + /** + * Register a renderer for a component type. + * @param target - The target type of component + * @param renderer - The render method. + */ + register(target: T, renderer: CustomComponentRenderFunction): void; + + /** + * Register a context menu section with a given set of items. + * @param items - A set of items to render. + */ + buildContextMenuBlock(items: ContextMenuItem[]): JSX.Element; +} diff --git a/packages/element-web-module-api/src/api/index.ts b/packages/element-web-module-api/src/api/index.ts index f20d9db300..35dd52d967 100644 --- a/packages/element-web-module-api/src/api/index.ts +++ b/packages/element-web-module-api/src/api/index.ts @@ -10,6 +10,7 @@ import { LegacyModuleApiExtension } from "./legacy-modules"; import { LegacyCustomisationsApiExtension } from "./legacy-customisations"; import { ConfigApi } from "./config"; import { I18nApi } from "./i18n"; +import { CustomComponentsApi, CustomComponentTarget } from "./custom-components"; /** * Module interface for modules to implement. @@ -86,6 +87,8 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx * @public */ readonly rootNode: HTMLElement; + + readonly customComponents: CustomComponentsApi; /** * 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/index.ts b/packages/element-web-module-api/src/index.ts index b503c88188..6640814d1f 100644 --- a/packages/element-web-module-api/src/index.ts +++ b/packages/element-web-module-api/src/index.ts @@ -9,5 +9,7 @@ export { ModuleLoader, ModuleIncompatibleError } from "./loader"; export type { Api, Module, ModuleFactory } from "./api"; export type { Config, ConfigApi } from "./api/config"; export type { I18nApi, Variables, Translations } from "./api/i18n"; +export type * from "./api/custom-components"; +export { CustomComponentTarget } from "./api/custom-components"; export type * from "./api/legacy-modules"; export type * from "./api/legacy-customisations"; From b5a7bd9eacffbafdc574899267bba73d6e7cff47 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 19 May 2025 09:16:27 +0100 Subject: [PATCH 02/14] Document and types' --- .../element-web-module-api.api.md | 29 +++++++-- packages/element-web-module-api/package.json | 4 +- .../src/api/custom-components.ts | 59 ++++++++++++++----- 3 files changed, 70 insertions(+), 22 deletions(-) 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 1e41a6da51..c86bf9cc60 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 @@ -6,6 +6,7 @@ import { JSX } from 'react'; import { ModuleApi } from '@matrix-org/react-sdk-module-api'; +import { RoomEvent } from 'matrix-js-sdk'; import { Root } from 'react-dom/client'; import { RuntimeModule } from '@matrix-org/react-sdk-module-api'; @@ -60,26 +61,42 @@ export interface ConfigApi { get(key?: K): Config | Config[K]; } +// @public (undocumented) +export interface ContextMenuItem { + iconClassName: string; + label: string; + onClick: (e: React.MouseEvent | React.KeyboardEvent | React.FormEvent) => void | Promise; +} + // @public export type CustomComponentProps = { - [CustomComponentTarget.TextualEvent]: { - mxEvent: any; - stripReply: boolean; + [CustomComponentTarget.TextualBody]: { + mxEvent: RoomEvent; + highlights?: string[]; + showUrlPreview?: boolean; + forExport?: boolean; + }; + [CustomComponentTarget.MessageContextMenu]: { + mxEvent: RoomEvent; + closeMenu: () => void; }; }; // @public -export type CustomComponentRenderFunction = (props: CustomComponentProps[T], originalComponent: JSX.Element) => JSX.Element | null; +export type CustomComponentRenderFunction = ( +props: CustomComponentProps[T], +originalComponent: JSX.Element) => JSX.Element | null; // @public export interface CustomComponentsApi { + buildContextMenuBlock(items: ContextMenuItem[]): JSX.Element; register(target: T, renderer: CustomComponentRenderFunction): void; } // @public export enum CustomComponentTarget { - // (undocumented) - TextualEvent = "TextualEvent" + MessageContextMenu = "MessageContextMenu", + TextualBody = "TextualBody" } // @alpha @deprecated (undocumented) diff --git a/packages/element-web-module-api/package.json b/packages/element-web-module-api/package.json index 9c0e0cc364..cd329b8788 100644 --- a/packages/element-web-module-api/package.json +++ b/packages/element-web-module-api/package.json @@ -44,13 +44,15 @@ "vite": "^6.0.11", "vite-plugin-dts": "^4.5.0", "vitest": "^3.0.5", - "vitest-sonar-reporter": "^2.0.0" + "vitest-sonar-reporter": "^2.0.0", + "matrix-js-sdk": "^37.5.0" }, "peerDependencies": { "@matrix-org/react-sdk-module-api": "*", "@types/react": "*", "@types/react-dom": "*", "matrix-web-i18n": "*", + "matrix-js-sdk": "*", "react": "^19" }, "peerDependenciesMeta": { 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 582a281ce1..f3cd115582 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -1,21 +1,38 @@ -import { ComponentType, JSX, MouseEventHandler, PropsWithChildren } from "react"; +import type { JSX } from "react"; +import type { RoomEvent } from "matrix-js-sdk"; /** - * Target of the renderer function. + * Targets in Element for custom components. * @public */ export enum CustomComponentTarget { /** - * TODO: We should make this more descriptive. - * The renderer for text events in the timeline, such as "m.room.message" + * Component that renders "m.room.message" events in the room timeline. */ TextualBody = "TextualBody", + /** + * "Options" Context menu for a timeline event. + * Use `buildContextMenuBlock` to build a section to be used by this component. + * @see buildContextMenuBlock + */ MessageContextMenu = "MessageContextMenu", } export interface ContextMenuItem { + /** + * The human readable label for an event. + * TODO: Should this be i18n-d + */ label: string; + /** + * The icon to use for this item. + * https://github.com/vector-im/riot-web/blob/efc6149a8b3362c01b93f52e76e5c4ae8cbcb65c/res/css/views/context_menus/_MessageContextMenu.pcss#L10 + */ iconClassName: string; + /** + * Handler for click events on the context menu item. + * Does NOT close the menu after execution. + */ onClick: ( e: React.MouseEvent | React.KeyboardEvent | React.FormEvent, ) => void | Promise; @@ -28,12 +45,12 @@ export interface ContextMenuItem { export type CustomComponentProps = { [CustomComponentTarget.TextualBody]: { /** - * The Matrix event information. - * TODO: Should this just use the types from matrix-js-sdk? + * The Matrix event for this textual body. */ - mxEvent: any; + mxEvent: RoomEvent; /** - * Words to highlight on + * Words to highlight on (e.g. from search results).\ + * May be undefined if the client does not need to highlight */ highlights?: string[]; /** @@ -47,12 +64,11 @@ export type CustomComponentProps = { }; [CustomComponentTarget.MessageContextMenu]: { /** - * The Matrix event information. - * TODO: Should this just use the types from matrix-js-sdk? + * The Matrix event which this context menu targets. */ - mxEvent: any; + mxEvent: RoomEvent; /** - * Close the menu + * Function that will close the menu. */ closeMenu: () => void; }; @@ -62,24 +78,37 @@ export type CustomComponentProps = { * @public */ export type CustomComponentRenderFunction = ( + /** + * Properties from the given target to be used for rendering. + */ props: CustomComponentProps[T], + /** + * The original component. + */ originalComponent: JSX.Element, ) => JSX.Element | null; /** - * The API for creating custom components. + * API for inserting custom components into Element. * @public */ export interface CustomComponentsApi { /** * Register a renderer for a component type. - * @param target - The target type of component + * The render function should either return a rendered component, or `null` if the + * component should not be overidden. + * + * Multiple render function may be registered for a single target, however the first + * non-null result will be used. If all results are null, or no registrations exist + * for a target then the original component is used. + * + * @param target - The target location for the component. * @param renderer - The render method. */ register(target: T, renderer: CustomComponentRenderFunction): void; /** - * Register a context menu section with a given set of items. + * Generate a context menu section for a given set of items. * @param items - A set of items to render. */ buildContextMenuBlock(items: ContextMenuItem[]): JSX.Element; From 50fc7bec66d94b96a0f7b74dc81d068827ee5f12 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 19 May 2025 11:56:53 +0100 Subject: [PATCH 03/14] lint --- packages/element-web-module-api/src/api/custom-components.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 f3cd115582..2d1f269409 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -18,6 +18,9 @@ export enum CustomComponentTarget { MessageContextMenu = "MessageContextMenu", } +/** + * @public + */ export interface ContextMenuItem { /** * The human readable label for an event. @@ -49,7 +52,7 @@ export type CustomComponentProps = { */ mxEvent: RoomEvent; /** - * Words to highlight on (e.g. from search results).\ + * Words to highlight on (e.g. from search results). * May be undefined if the client does not need to highlight */ highlights?: string[]; From e972c05ccc3667ef3292f0ebc6f3f9389b3577da Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 20 May 2025 12:11:07 +0100 Subject: [PATCH 04/14] Remove context menu --- .../element-web-module-api.api.md | 19 ++----- .../src/api/custom-components.ts | 53 ++----------------- 2 files changed, 7 insertions(+), 65 deletions(-) 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 c86bf9cc60..f9731b1371 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 @@ -5,8 +5,8 @@ ```ts import { JSX } from 'react'; +import { MatrixEvent } from 'matrix-js-sdk'; import { ModuleApi } from '@matrix-org/react-sdk-module-api'; -import { RoomEvent } from 'matrix-js-sdk'; import { Root } from 'react-dom/client'; import { RuntimeModule } from '@matrix-org/react-sdk-module-api'; @@ -61,41 +61,28 @@ export interface ConfigApi { get(key?: K): Config | Config[K]; } -// @public (undocumented) -export interface ContextMenuItem { - iconClassName: string; - label: string; - onClick: (e: React.MouseEvent | React.KeyboardEvent | React.FormEvent) => void | Promise; -} - // @public export type CustomComponentProps = { [CustomComponentTarget.TextualBody]: { - mxEvent: RoomEvent; + mxEvent: MatrixEvent; highlights?: string[]; showUrlPreview?: boolean; forExport?: boolean; }; - [CustomComponentTarget.MessageContextMenu]: { - mxEvent: RoomEvent; - closeMenu: () => void; - }; }; // @public export type CustomComponentRenderFunction = ( props: CustomComponentProps[T], -originalComponent: JSX.Element) => JSX.Element | null; +originalComponent: () => JSX.Element) => JSX.Element | null; // @public export interface CustomComponentsApi { - buildContextMenuBlock(items: ContextMenuItem[]): JSX.Element; register(target: T, renderer: CustomComponentRenderFunction): void; } // @public export enum CustomComponentTarget { - MessageContextMenu = "MessageContextMenu", TextualBody = "TextualBody" } 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 2d1f269409..244757c6e1 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -1,5 +1,5 @@ import type { JSX } from "react"; -import type { RoomEvent } from "matrix-js-sdk"; +import type { MatrixEvent } from "matrix-js-sdk"; /** * Targets in Element for custom components. @@ -10,35 +10,6 @@ export enum CustomComponentTarget { * Component that renders "m.room.message" events in the room timeline. */ TextualBody = "TextualBody", - /** - * "Options" Context menu for a timeline event. - * Use `buildContextMenuBlock` to build a section to be used by this component. - * @see buildContextMenuBlock - */ - MessageContextMenu = "MessageContextMenu", -} - -/** - * @public - */ -export interface ContextMenuItem { - /** - * The human readable label for an event. - * TODO: Should this be i18n-d - */ - label: string; - /** - * The icon to use for this item. - * https://github.com/vector-im/riot-web/blob/efc6149a8b3362c01b93f52e76e5c4ae8cbcb65c/res/css/views/context_menus/_MessageContextMenu.pcss#L10 - */ - iconClassName: string; - /** - * Handler for click events on the context menu item. - * Does NOT close the menu after execution. - */ - onClick: ( - e: React.MouseEvent | React.KeyboardEvent | React.FormEvent, - ) => void | Promise; } /** @@ -50,7 +21,7 @@ export type CustomComponentProps = { /** * The Matrix event for this textual body. */ - mxEvent: RoomEvent; + mxEvent: MatrixEvent; /** * Words to highlight on (e.g. from search results). * May be undefined if the client does not need to highlight @@ -65,16 +36,6 @@ export type CustomComponentProps = { */ forExport?: boolean; }; - [CustomComponentTarget.MessageContextMenu]: { - /** - * The Matrix event which this context menu targets. - */ - mxEvent: RoomEvent; - /** - * Function that will close the menu. - */ - closeMenu: () => void; - }; }; /** * Render function. Returning null skips this function and passes it onto the next registered renderer. @@ -86,9 +47,9 @@ export type CustomComponentRenderFunction = ( */ props: CustomComponentProps[T], /** - * The original component. + * Render function for the original component. */ - originalComponent: JSX.Element, + originalComponent: () => JSX.Element, ) => JSX.Element | null; /** @@ -109,10 +70,4 @@ export interface CustomComponentsApi { * @param renderer - The render method. */ register(target: T, renderer: CustomComponentRenderFunction): void; - - /** - * Generate a context menu section for a given set of items. - * @param items - A set of items to render. - */ - buildContextMenuBlock(items: ContextMenuItem[]): JSX.Element; } From 908771235f48139581d887f903f8d829926876fb Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 20 May 2025 12:14:20 +0100 Subject: [PATCH 05/14] Tidyup --- .../element-web-module-api/src/api/custom-components.ts | 7 +++++++ packages/element-web-module-api/src/api/index.ts | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) 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 244757c6e1..1c1c68b91b 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -1,3 +1,10 @@ +/* +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 type { JSX } from "react"; import type { MatrixEvent } from "matrix-js-sdk"; diff --git a/packages/element-web-module-api/src/api/index.ts b/packages/element-web-module-api/src/api/index.ts index 35dd52d967..feb97cbe51 100644 --- a/packages/element-web-module-api/src/api/index.ts +++ b/packages/element-web-module-api/src/api/index.ts @@ -10,7 +10,7 @@ import { LegacyModuleApiExtension } from "./legacy-modules"; import { LegacyCustomisationsApiExtension } from "./legacy-customisations"; import { ConfigApi } from "./config"; import { I18nApi } from "./i18n"; -import { CustomComponentsApi, CustomComponentTarget } from "./custom-components"; +import { CustomComponentsApi } from "./custom-components"; /** * Module interface for modules to implement. @@ -88,6 +88,10 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx */ readonly rootNode: HTMLElement; + /** + * The custom components API. + * @public + */ readonly customComponents: CustomComponentsApi; /** * Create a ReactDOM root for rendering React components. From ba5ce17574a55f0432075472f33e80a8ed9db582 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 20 May 2025 13:00:47 +0100 Subject: [PATCH 06/14] docs fix --- packages/element-web-module-api/element-web-module-api.api.md | 1 - 1 file changed, 1 deletion(-) 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 f9731b1371..18cfa96c1b 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 @@ -23,7 +23,6 @@ export interface AliasCustomisations { export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension { readonly config: ConfigApi; createRoot(element: Element): Root; - // (undocumented) readonly customComponents: CustomComponentsApi; readonly i18n: I18nApi; readonly rootNode: HTMLElement; From 930a5eb8b70499d5c91d8c757a64c31de564bc09 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 30 May 2025 14:51:28 +0100 Subject: [PATCH 07/14] Update module API --- .../element-web-module-api.api.md | 38 +++++----- .../src/api/custom-components.ts | 76 ++++++++----------- .../element-web-module-api/src/api/index.ts | 2 +- packages/element-web-module-api/src/index.ts | 1 - 4 files changed, 51 insertions(+), 66 deletions(-) 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 18cfa96c1b..2914fc8603 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 @@ -61,29 +61,25 @@ export interface ConfigApi { } // @public -export type CustomComponentProps = { - [CustomComponentTarget.TextualBody]: { - mxEvent: MatrixEvent; - highlights?: string[]; - showUrlPreview?: boolean; - forExport?: boolean; - }; +export interface CustomComponentsApi { + // Warning: (ae-incompatible-release-tags) The symbol "registerMessageRenderer" is marked as @public, but its signature references "CustomMessageRenderFunction" which is marked as @beta + registerMessageRenderer(eventType: string | RegExp, renderer: CustomMessageRenderFunction): void; +} + +// @alpha +export type CustomMessageComponentProps = { + mxEvent: MatrixEvent; + highlights?: string[]; + showUrlPreview?: boolean; + forExport?: boolean; }; -// @public -export type CustomComponentRenderFunction = ( -props: CustomComponentProps[T], -originalComponent: () => JSX.Element) => JSX.Element | null; - -// @public -export interface CustomComponentsApi { - register(target: T, renderer: CustomComponentRenderFunction): void; -} - -// @public -export enum CustomComponentTarget { - TextualBody = "TextualBody" -} +// Warning: (ae-incompatible-release-tags) The symbol "CustomMessageRenderFunction" is marked as @beta, but its signature references "CustomMessageComponentProps" which is marked as @alpha +// +// @beta +export type CustomMessageRenderFunction = ( +props: CustomMessageComponentProps, +originalComponent?: () => React.JSX.Element) => JSX.Element | null; // @alpha @deprecated (undocumented) export interface DirectoryCustomisations { 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 1c1c68b91b..3cae10b61c 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -9,54 +9,42 @@ import type { JSX } from "react"; import type { MatrixEvent } from "matrix-js-sdk"; /** - * Targets in Element for custom components. - * @public + * Properties for all message components. + * @alpha Subject to change. */ -export enum CustomComponentTarget { +export type CustomMessageComponentProps = { /** - * Component that renders "m.room.message" events in the room timeline. + * The Matrix event for this textual body. */ - TextualBody = "TextualBody", -} + mxEvent: MatrixEvent; + /** + * Words to highlight on (e.g. from search results). + * May be undefined if the client does not need to highlight + */ + highlights?: string[]; + /** + * Should previews be shown for this event + */ + showUrlPreview?: boolean; + /** + * Is this event being rendered to a static export + */ + forExport?: boolean; +}; /** - * Properties for the render component. - * @public + * Function used to render a message component. + * @beta Unlikely to change */ -export type CustomComponentProps = { - [CustomComponentTarget.TextualBody]: { - /** - * The Matrix event for this textual body. - */ - mxEvent: MatrixEvent; - /** - * Words to highlight on (e.g. from search results). - * May be undefined if the client does not need to highlight - */ - highlights?: string[]; - /** - * Should previews be shown for this event - */ - showUrlPreview?: boolean; - /** - * Is this event being rendered to a static export - */ - forExport?: boolean; - }; -}; -/** - * Render function. Returning null skips this function and passes it onto the next registered renderer. - * @public - */ -export type CustomComponentRenderFunction = ( +export type CustomMessageRenderFunction = ( /** - * Properties from the given target to be used for rendering. + * Properties for the message to be renderered. */ - props: CustomComponentProps[T], + props: CustomMessageComponentProps, /** - * Render function for the original component. + * Render function for the original component. This may be omitted if the message would not normally be rendered. */ - originalComponent: () => JSX.Element, + originalComponent?: () => React.JSX.Element, ) => JSX.Element | null; /** @@ -65,16 +53,18 @@ export type CustomComponentRenderFunction = ( */ export interface CustomComponentsApi { /** - * Register a renderer for a component type. + * Register a renderer for a message type in the timeline. + * * The render function should either return a rendered component, or `null` if the - * component should not be overidden. + * component should not be overidden (for instance, to passthrough to another module or allow + * the application complete control) * * Multiple render function may be registered for a single target, however the first * non-null result will be used. If all results are null, or no registrations exist * for a target then the original component is used. * - * @param target - The target location for the component. - * @param renderer - The render method. + * @param eventType - The event type this renderer is for. Use a RegExp instance if you want to target multiple types. + * @param renderer - The render function. */ - register(target: T, renderer: CustomComponentRenderFunction): void; + registerMessageRenderer(eventType: string | RegExp, renderer: CustomMessageRenderFunction): void; } diff --git a/packages/element-web-module-api/src/api/index.ts b/packages/element-web-module-api/src/api/index.ts index feb97cbe51..3e9d4d072f 100644 --- a/packages/element-web-module-api/src/api/index.ts +++ b/packages/element-web-module-api/src/api/index.ts @@ -89,7 +89,7 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx readonly rootNode: HTMLElement; /** - * The custom components API. + * The custom message component API. * @public */ readonly customComponents: CustomComponentsApi; diff --git a/packages/element-web-module-api/src/index.ts b/packages/element-web-module-api/src/index.ts index 6640814d1f..4fbf15e9ba 100644 --- a/packages/element-web-module-api/src/index.ts +++ b/packages/element-web-module-api/src/index.ts @@ -10,6 +10,5 @@ export type { Api, Module, ModuleFactory } from "./api"; export type { Config, ConfigApi } from "./api/config"; export type { I18nApi, Variables, Translations } from "./api/i18n"; export type * from "./api/custom-components"; -export { CustomComponentTarget } from "./api/custom-components"; export type * from "./api/legacy-modules"; export type * from "./api/legacy-customisations"; From 546861811c048a794898ca16c06b831e66605c8a Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 3 Jun 2025 13:41:57 +0100 Subject: [PATCH 08/14] fixup --- packages/element-web-module-api/element-web-module-api.api.md | 2 +- packages/element-web-module-api/src/api/custom-components.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 2914fc8603..f7160e5289 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 @@ -5,7 +5,7 @@ ```ts import { JSX } from 'react'; -import { MatrixEvent } from 'matrix-js-sdk'; +import { MatrixEvent } from 'matrix-js-sdk/lib/matrix'; import { ModuleApi } from '@matrix-org/react-sdk-module-api'; import { Root } from 'react-dom/client'; import { RuntimeModule } from '@matrix-org/react-sdk-module-api'; 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 3cae10b61c..4f008acafe 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import type { JSX } from "react"; -import type { MatrixEvent } from "matrix-js-sdk"; +import type { MatrixEvent } from "matrix-js-sdk/lib/matrix"; /** * Properties for all message components. From e2ae6f2288560d2f60859b1b6d9f420760472197 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 5 Jun 2025 09:58:51 +0100 Subject: [PATCH 09/14] Specify options for the original component. --- .../element-web-module-api.api.md | 9 +++++++-- packages/element-web-module-api/package.json | 6 +++--- .../src/api/custom-components.ts | 17 ++++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) 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 f7160e5289..90cd4df08f 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 @@ -70,16 +70,16 @@ export interface CustomComponentsApi { export type CustomMessageComponentProps = { mxEvent: MatrixEvent; highlights?: string[]; - showUrlPreview?: boolean; forExport?: boolean; }; // Warning: (ae-incompatible-release-tags) The symbol "CustomMessageRenderFunction" is marked as @beta, but its signature references "CustomMessageComponentProps" which is marked as @alpha +// Warning: (ae-incompatible-release-tags) The symbol "CustomMessageRenderFunction" is marked as @beta, but its signature references "OriginalComponentProps" which is marked as @alpha // // @beta export type CustomMessageRenderFunction = ( props: CustomMessageComponentProps, -originalComponent?: () => React.JSX.Element) => JSX.Element | null; +originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element) => JSX.Element | null; // @alpha @deprecated (undocumented) export interface DirectoryCustomisations { @@ -205,6 +205,11 @@ export class ModuleLoader { start(): Promise; } +// @alpha +export type OriginalComponentProps = { + showUrlPreview?: boolean; +}; + // @alpha @deprecated (undocumented) export interface RoomListCustomisations { isRoomVisible?(room: Room): boolean; diff --git a/packages/element-web-module-api/package.json b/packages/element-web-module-api/package.json index f2fed95dfb..94d1d4913d 100644 --- a/packages/element-web-module-api/package.json +++ b/packages/element-web-module-api/package.json @@ -38,21 +38,21 @@ "@types/react-dom": "^19.0.4", "@types/semver": "^7.5.8", "@vitest/coverage-v8": "^3.0.4", + "matrix-js-sdk": "^37.5.0", "matrix-web-i18n": "^3.3.0", "semver": "^7.6.3", "typescript": "^5.7.3", "vite": "^6.1.6", "vite-plugin-dts": "^4.5.0", "vitest": "^3.0.5", - "vitest-sonar-reporter": "^2.0.0", - "matrix-js-sdk": "^37.5.0" + "vitest-sonar-reporter": "^2.0.0" }, "peerDependencies": { "@matrix-org/react-sdk-module-api": "*", "@types/react": "*", "@types/react-dom": "*", - "matrix-web-i18n": "*", "matrix-js-sdk": "*", + "matrix-web-i18n": "*", "react": "^19" }, "peerDependenciesMeta": { 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 4f008acafe..5ce0f8fefd 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -22,16 +22,23 @@ export type CustomMessageComponentProps = { * May be undefined if the client does not need to highlight */ highlights?: string[]; - /** - * Should previews be shown for this event - */ - showUrlPreview?: boolean; /** * Is this event being rendered to a static export */ forExport?: boolean; }; +/** + * Properties to alter the render function of the original component. + * @alpha Subject to change. + */ +export type OriginalComponentProps = { + /** + * Should previews be shown for this event. + */ + showUrlPreview?: boolean; +}; + /** * Function used to render a message component. * @beta Unlikely to change @@ -44,7 +51,7 @@ export type CustomMessageRenderFunction = ( /** * Render function for the original component. This may be omitted if the message would not normally be rendered. */ - originalComponent?: () => React.JSX.Element, + originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element, ) => JSX.Element | null; /** From c6a612382630dbb38d037e0f3c41a5baaafba03a Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 5 Jun 2025 10:01:40 +0100 Subject: [PATCH 10/14] Remove unused props --- .../src/api/custom-components.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) 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 5ce0f8fefd..f9f4128c91 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -17,15 +17,6 @@ export type CustomMessageComponentProps = { * The Matrix event for this textual body. */ mxEvent: MatrixEvent; - /** - * Words to highlight on (e.g. from search results). - * May be undefined if the client does not need to highlight - */ - highlights?: string[]; - /** - * Is this event being rendered to a static export - */ - forExport?: boolean; }; /** @@ -35,6 +26,7 @@ export type CustomMessageComponentProps = { export type OriginalComponentProps = { /** * Should previews be shown for this event. + * This may be overriden by user preferences. */ showUrlPreview?: boolean; }; From 60687b27a6f6967e6ae5c92ad77448c5a50e4bfb Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 5 Jun 2025 16:10:35 +0100 Subject: [PATCH 11/14] Add an example --- .../element-web-module-api/src/api/custom-components.ts | 9 +++++++++ 1 file changed, 9 insertions(+) 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 f9f4128c91..01d589341e 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -64,6 +64,15 @@ export interface CustomComponentsApi { * * @param eventType - The event type this renderer is for. Use a RegExp instance if you want to target multiple types. * @param renderer - The render function. + * @example + * ``` + * customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => { + * return ; + * }); + * customComponents.registerMessageRenderer(/m\.room\.(topic|name)/, (props, originalComponent) => { + * return ; + * }); + * ``` */ registerMessageRenderer(eventType: string | RegExp, renderer: CustomMessageRenderFunction): void; } From 8cf426f0e77dbc857a695715c19791b333a1e063 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 5 Jun 2025 16:57:48 +0100 Subject: [PATCH 12/14] tighten example --- .../element-web-module-api/element-web-module-api.api.md | 2 -- .../element-web-module-api/src/api/custom-components.ts | 6 +++++- 2 files changed, 5 insertions(+), 3 deletions(-) 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 90cd4df08f..49ce6f22cf 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 @@ -69,8 +69,6 @@ export interface CustomComponentsApi { // @alpha export type CustomMessageComponentProps = { mxEvent: MatrixEvent; - highlights?: string[]; - forExport?: boolean; }; // Warning: (ae-incompatible-release-tags) The symbol "CustomMessageRenderFunction" is marked as @beta, but its signature references "CustomMessageComponentProps" which is marked as @alpha 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 01d589341e..87266de5ad 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -70,7 +70,11 @@ export interface CustomComponentsApi { * return ; * }); * customComponents.registerMessageRenderer(/m\.room\.(topic|name)/, (props, originalComponent) => { - * return ; + * if (props.mxEvent.isState()) { + * return ; + * } + * // Passthrough. + * return null; * }); * ``` */ From 240045f4337e6b6a7ead4b6687612b4dd144afd2 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 9 Jun 2025 15:41:53 +0100 Subject: [PATCH 13/14] Add filter function / hints --- .../element-web-module-api.api.md | 17 ++++--- .../src/api/custom-components.ts | 50 ++++++++++++------- 2 files changed, 43 insertions(+), 24 deletions(-) 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 49ce6f22cf..93b565b681 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 @@ -62,8 +62,9 @@ export interface ConfigApi { // @public export interface CustomComponentsApi { - // Warning: (ae-incompatible-release-tags) The symbol "registerMessageRenderer" is marked as @public, but its signature references "CustomMessageRenderFunction" which is marked as @beta - registerMessageRenderer(eventType: string | RegExp, renderer: CustomMessageRenderFunction): void; + // Warning: (ae-incompatible-release-tags) The symbol "registerMessageRenderer" is marked as @public, but its signature references "CustomMessageRenderFunction" which is marked as @alpha + // Warning: (ae-incompatible-release-tags) The symbol "registerMessageRenderer" is marked as @public, but its signature references "CustomMessageRenderHints" which is marked as @alpha + registerMessageRenderer(eventType: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints?: CustomMessageRenderHints): void; } // @alpha @@ -71,13 +72,15 @@ export type CustomMessageComponentProps = { mxEvent: MatrixEvent; }; -// Warning: (ae-incompatible-release-tags) The symbol "CustomMessageRenderFunction" is marked as @beta, but its signature references "CustomMessageComponentProps" which is marked as @alpha -// Warning: (ae-incompatible-release-tags) The symbol "CustomMessageRenderFunction" is marked as @beta, but its signature references "OriginalComponentProps" which is marked as @alpha -// -// @beta +// @alpha export type CustomMessageRenderFunction = ( props: CustomMessageComponentProps, -originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element) => JSX.Element | null; +originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element) => JSX.Element; + +// @alpha +export type CustomMessageRenderHints = { + allowEditingEvent?: boolean; +}; // @alpha @deprecated (undocumented) export interface DirectoryCustomisations { 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 87266de5ad..794786538a 100644 --- a/packages/element-web-module-api/src/api/custom-components.ts +++ b/packages/element-web-module-api/src/api/custom-components.ts @@ -15,6 +15,7 @@ import type { MatrixEvent } from "matrix-js-sdk/lib/matrix"; export type CustomMessageComponentProps = { /** * The Matrix event for this textual body. + * @alpha */ mxEvent: MatrixEvent; }; @@ -31,9 +32,23 @@ export type OriginalComponentProps = { showUrlPreview?: boolean; }; +/** + * Hints to specify to Element when rendering events. + * @alpha Subject to change. + */ +export type CustomMessageRenderHints = { + /** + * Should the event be allowed to be edited in the client. This should + * be set to false if you override the render function, as the module + * API has no way to display message editing at the moment. + * Default is true. + */ + allowEditingEvent?: boolean; +}; + /** * Function used to render a message component. - * @beta Unlikely to change + * @alpha Unlikely to change */ export type CustomMessageRenderFunction = ( /** @@ -44,7 +59,7 @@ 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, -) => JSX.Element | null; +) => JSX.Element; /** * API for inserting custom components into Element. @@ -54,29 +69,30 @@ export interface CustomComponentsApi { /** * Register a renderer for a message type in the timeline. * - * The render function should either return a rendered component, or `null` if the - * component should not be overidden (for instance, to passthrough to another module or allow - * the application complete control) + * The render function should return a rendered component. * - * Multiple render function may be registered for a single target, however the first - * non-null result will be used. If all results are null, or no registrations exist - * for a target then the original component is used. + * Multiple render function may be registered for a single event type, however the first matching + * result will be used. If no events match or are registered then the originalComponent is rendered. * - * @param eventType - The event type this renderer is for. Use a RegExp instance if you want to target multiple types. + * @param eventTypeOrFilter - The event type this renderer is for. Use a function for more complex filtering. * @param renderer - The render function. + * @param hints - Hints that alter the way the tile is handled. * @example * ``` * customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => { * return ; * }); - * customComponents.registerMessageRenderer(/m\.room\.(topic|name)/, (props, originalComponent) => { - * if (props.mxEvent.isState()) { - * return ; - * } - * // Passthrough. - * return null; - * }); + * customComponents.registerMessageRenderer( + * (mxEvent) => mxEvent.getType().matches(/m\.room\.(topic|name)/) && mxEvent.isState(), + * (props, originalComponent) => { + * return ; + * } + * ); * ``` */ - registerMessageRenderer(eventType: string | RegExp, renderer: CustomMessageRenderFunction): void; + registerMessageRenderer( + eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), + renderer: CustomMessageRenderFunction, + hints?: CustomMessageRenderHints, + ): void; } From cea6abfca5252b2e700e30296c71383e9c1b47c9 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 12 Jun 2025 10:58:45 +0100 Subject: [PATCH 14/14] Update API docs --- packages/element-web-module-api/element-web-module-api.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 93b565b681..754a4d2eec 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 @@ -64,7 +64,7 @@ export interface ConfigApi { export interface CustomComponentsApi { // Warning: (ae-incompatible-release-tags) The symbol "registerMessageRenderer" is marked as @public, but its signature references "CustomMessageRenderFunction" which is marked as @alpha // Warning: (ae-incompatible-release-tags) The symbol "registerMessageRenderer" is marked as @public, but its signature references "CustomMessageRenderHints" which is marked as @alpha - registerMessageRenderer(eventType: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints?: CustomMessageRenderHints): void; + registerMessageRenderer(eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints?: CustomMessageRenderHints): void; } // @alpha