Add a module API for overriding the composer preview. (#33978)

* Modify LinkPreview to export shared atomics

* Implement MessageComposerUrlPreviewView

* Create UrlPreviewFetcher utility function

* Modify view models

* Implement in composer

* Support running tests in dom-less vitest environment

* Add a playwright test

* hide another one

* fmt

* cleanup

* test rte too

* fixup

* Add back docstring

* cleanup

* off by one

* remove description check

* Cleanup hacks

* Remove another hack

* cleanup

* one more

* fixup window here too

* whoops type

* Rename to be cleaeer

* Trim URLs first

* fix bug

* Add a module API for overriding the composer preview.

* tests + cleanup

* fixup comment

* revert accidental change

* fixup

* happy tests

* cleanup
This commit is contained in:
Will Hunt
2026-07-06 12:32:07 +00:00
committed by GitHub
parent 6027ec5142
commit b6cbc3a9d8
5 changed files with 246 additions and 16 deletions
+39 -3
View File
@@ -17,10 +17,13 @@ import type {
MatrixEvent as ModuleMatrixEvent,
CustomRoomPreviewBarRenderFunction,
CustomLoginRenderFunction,
CustomComposerPreviewRenderFunction,
CustomComposerPreviewComponentProps,
} from "@element-hq/element-web-module-api";
import type React from "react";
type EventTypeOrFilter = Parameters<ICustomComponentsApi["registerMessageRenderer"]>[0];
type ComposerPreviewFilterFn = Parameters<ICustomComponentsApi["registerComposerPreview"]>[0];
type EventRenderer = {
eventTypeOrFilter: EventTypeOrFilter;
@@ -37,6 +40,11 @@ interface CustomMessageRenderHints extends Omit<ModuleCustomCustomMessageRenderH
allowDownloadingMedia?: () => Promise<boolean>;
}
type ComposerPreviewRenderer = {
filter: ComposerPreviewFilterFn;
renderer: CustomComposerPreviewRenderFunction;
};
export class CustomComponentsApi implements ICustomComponentsApi {
/**
* Convert a matrix-js-sdk event into a ModuleMatrixEvent.
@@ -66,6 +74,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
}
private readonly registeredMessageRenderers: EventRenderer[] = [];
private readonly registeredComposerPreviewRenderers: ComposerPreviewRenderer[] = [];
public registerMessageRenderer(
eventTypeOrFilter: EventTypeOrFilter,
@@ -80,7 +89,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
* @param mxEvent The message event being rendered.
* @returns The registered renderer.
*/
private selectRenderer(mxEvent: ModuleMatrixEvent): EventRenderer | undefined {
private selectMessageRenderer(mxEvent: ModuleMatrixEvent): EventRenderer | undefined {
return this.registeredMessageRenderers.find((renderer) => {
if (typeof renderer.eventTypeOrFilter === "string") {
return renderer.eventTypeOrFilter === mxEvent.type;
@@ -106,7 +115,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
originalComponent?: (props?: OriginalMessageComponentProps) => React.JSX.Element,
): React.JSX.Element | null {
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(props.mxEvent);
const renderer = moduleEv && this.selectRenderer(moduleEv);
const renderer = moduleEv && this.selectMessageRenderer(moduleEv);
if (renderer) {
try {
return renderer.renderer({ ...props, mxEvent: moduleEv }, originalComponent);
@@ -125,7 +134,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
*/
public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints | null {
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(mxEvent);
const renderer = moduleEv && this.selectRenderer(moduleEv);
const renderer = moduleEv && this.selectMessageRenderer(moduleEv);
if (renderer) {
return {
...renderer.hints,
@@ -171,4 +180,31 @@ export class CustomComponentsApi implements ICustomComponentsApi {
public registerLoginComponent(renderer: CustomLoginRenderFunction): void {
this._loginRenderer = renderer;
}
public registerComposerPreview(
filter: ComposerPreviewFilterFn,
renderer: CustomComposerPreviewRenderFunction,
): void {
this.registeredComposerPreviewRenderers.push({ filter, renderer });
}
/**
* Render the component for a composer preview.
* @param props Props to be passed to the custom renderer.
* @param originalComponent Function that will be rendered if no custom renderers are present, or as a child of a custom component.
* @returns A component if a custom renderer was found. Otherwise null.
*/
public renderComposerPreview(
props: CustomComposerPreviewComponentProps,
originalComponent: (props?: CustomComposerPreviewComponentProps) => React.JSX.Element,
): React.JSX.Element | null {
const renderer = this.registeredComposerPreviewRenderers.find(({ filter }) => filter(props.text, props.roomId));
if (renderer) {
try {
return renderer.renderer({ ...props }, originalComponent);
} catch (ex) {
logger.warn("Composer preview failed to render", ex);
}
}
return null;
}
}