Merge pull request #32 from element-hq/hs/custom-component-api

Support custom components for messages
This commit is contained in:
Will Hunt
2025-06-12 14:23:36 +01:00
committed by GitHub
5 changed files with 371 additions and 233 deletions
@@ -4,6 +4,8 @@
```ts ```ts
import { JSX } from 'react';
import { MatrixEvent } from 'matrix-js-sdk/lib/matrix';
import { ModuleApi } from '@matrix-org/react-sdk-module-api'; import { ModuleApi } from '@matrix-org/react-sdk-module-api';
import { Root } from 'react-dom/client'; import { Root } from 'react-dom/client';
import { RuntimeModule } from '@matrix-org/react-sdk-module-api'; import { RuntimeModule } from '@matrix-org/react-sdk-module-api';
@@ -21,6 +23,7 @@ export interface AliasCustomisations {
export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension { export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiExtension {
readonly config: ConfigApi; readonly config: ConfigApi;
createRoot(element: Element): Root; createRoot(element: Element): Root;
readonly customComponents: CustomComponentsApi;
readonly i18n: I18nApi; readonly i18n: I18nApi;
readonly rootNode: HTMLElement; readonly rootNode: HTMLElement;
} }
@@ -57,6 +60,28 @@ export interface ConfigApi {
get<K extends keyof Config = never>(key?: K): Config | Config[K]; get<K extends keyof Config = never>(key?: K): Config | Config[K];
} }
// @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 @alpha
// Warning: (ae-incompatible-release-tags) The symbol "registerMessageRenderer" is marked as @public, but its signature references "CustomMessageRenderHints" which is marked as @alpha
registerMessageRenderer(eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints?: CustomMessageRenderHints): void;
}
// @alpha
export type CustomMessageComponentProps = {
mxEvent: MatrixEvent;
};
// @alpha
export type CustomMessageRenderFunction = (
props: CustomMessageComponentProps,
originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element) => JSX.Element;
// @alpha
export type CustomMessageRenderHints = {
allowEditingEvent?: boolean;
};
// @alpha @deprecated (undocumented) // @alpha @deprecated (undocumented)
export interface DirectoryCustomisations { export interface DirectoryCustomisations {
// (undocumented) // (undocumented)
@@ -181,6 +206,11 @@ export class ModuleLoader {
start(): Promise<void>; start(): Promise<void>;
} }
// @alpha
export type OriginalComponentProps = {
showUrlPreview?: boolean;
};
// @alpha @deprecated (undocumented) // @alpha @deprecated (undocumented)
export interface RoomListCustomisations<Room> { export interface RoomListCustomisations<Room> {
isRoomVisible?(room: Room): boolean; isRoomVisible?(room: Room): boolean;
@@ -38,6 +38,7 @@
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@vitest/coverage-v8": "^3.0.4", "@vitest/coverage-v8": "^3.0.4",
"matrix-js-sdk": "^37.5.0",
"matrix-web-i18n": "^3.3.0", "matrix-web-i18n": "^3.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"typescript": "^5.7.3", "typescript": "^5.7.3",
@@ -50,6 +51,7 @@
"@matrix-org/react-sdk-module-api": "*", "@matrix-org/react-sdk-module-api": "*",
"@types/react": "*", "@types/react": "*",
"@types/react-dom": "*", "@types/react-dom": "*",
"matrix-js-sdk": "*",
"matrix-web-i18n": "*", "matrix-web-i18n": "*",
"react": "^19" "react": "^19"
}, },
@@ -0,0 +1,98 @@
/*
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/lib/matrix";
/**
* Properties for all message components.
* @alpha Subject to change.
*/
export type CustomMessageComponentProps = {
/**
* The Matrix event for this textual body.
* @alpha
*/
mxEvent: MatrixEvent;
};
/**
* Properties to alter the render function of the original component.
* @alpha Subject to change.
*/
export type OriginalComponentProps = {
/**
* Should previews be shown for this event.
* This may be overriden by user preferences.
*/
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.
* @alpha Unlikely to change
*/
export type CustomMessageRenderFunction = (
/**
* Properties for the message to be renderered.
*/
props: CustomMessageComponentProps,
/**
* 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;
/**
* API for inserting custom components into Element.
* @public
*/
export interface CustomComponentsApi {
/**
* Register a renderer for a message type in the timeline.
*
* The render function should return a rendered component.
*
* 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 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 <YourCustomComponent mxEvent={props.mxEvent} />;
* });
* customComponents.registerMessageRenderer(
* (mxEvent) => mxEvent.getType().matches(/m\.room\.(topic|name)/) && mxEvent.isState(),
* (props, originalComponent) => {
* return <YourCustomStateRenderer mxEvent={props.mxEvent} />;
* }
* );
* ```
*/
registerMessageRenderer(
eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean),
renderer: CustomMessageRenderFunction,
hints?: CustomMessageRenderHints,
): void;
}
@@ -10,6 +10,7 @@ import { LegacyModuleApiExtension } from "./legacy-modules";
import { LegacyCustomisationsApiExtension } from "./legacy-customisations"; import { LegacyCustomisationsApiExtension } from "./legacy-customisations";
import { ConfigApi } from "./config"; import { ConfigApi } from "./config";
import { I18nApi } from "./i18n"; import { I18nApi } from "./i18n";
import { CustomComponentsApi } from "./custom-components";
/** /**
* Module interface for modules to implement. * Module interface for modules to implement.
@@ -86,6 +87,12 @@ export interface Api extends LegacyModuleApiExtension, LegacyCustomisationsApiEx
* @public * @public
*/ */
readonly rootNode: HTMLElement; readonly rootNode: HTMLElement;
/**
* The custom message component API.
* @public
*/
readonly customComponents: CustomComponentsApi;
/** /**
* Create a ReactDOM root for rendering React components. * Create a ReactDOM root for rendering React components.
* Exposed to allow modules to avoid needing to bundle their own ReactDOM. * Exposed to allow modules to avoid needing to bundle their own ReactDOM.
@@ -9,5 +9,6 @@ export { ModuleLoader, ModuleIncompatibleError } from "./loader";
export type { Api, Module, ModuleFactory } from "./api"; export type { Api, Module, ModuleFactory } from "./api";
export type { Config, ConfigApi } from "./api/config"; export type { Config, ConfigApi } from "./api/config";
export type { I18nApi, Variables, Translations } from "./api/i18n"; export type { I18nApi, Variables, Translations } from "./api/i18n";
export type * from "./api/custom-components";
export type * from "./api/legacy-modules"; export type * from "./api/legacy-modules";
export type * from "./api/legacy-customisations"; export type * from "./api/legacy-customisations";