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:
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render, waitFor } from "test-utils-rtl";
|
||||||
|
import { test, describe, beforeEach, expect, vi, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import { MessageComposerUrlPreviewWrapper, DEBOUNCE_REQUEST_TIMEOUT_MS } from "./MessageComposerUrlPreview";
|
||||||
|
import {
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
getRoomContext,
|
||||||
|
mkRoom,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
} from "../../../../test/test-utils";
|
||||||
|
import type { I18nApi } from "@element-hq/element-web-module-api";
|
||||||
|
import type { ModuleApi } from "../../../modules/Api";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext";
|
||||||
|
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { CustomComponentsApi } from "../../../modules/customComponentApi";
|
||||||
|
|
||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
const BASIC_PREVIEW_OGDATA = {
|
||||||
|
"og:title": "This is an example!",
|
||||||
|
"og:description": "This is a description",
|
||||||
|
"og:type": "document",
|
||||||
|
"og:url": "https://example.org",
|
||||||
|
"og:site_name": "Example.org",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("MessageComposerUrlPreview", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
let originalMxModuleApi: ModuleApi;
|
||||||
|
beforeEach(() => {
|
||||||
|
originalMxModuleApi = window.mxModuleApi;
|
||||||
|
window.mxModuleApi = {
|
||||||
|
i18n: {} as I18nApi,
|
||||||
|
} as ModuleApi;
|
||||||
|
client = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(),
|
||||||
|
getUrlPreview: vi.fn().mockResolvedValue(BASIC_PREVIEW_OGDATA),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
window.mxModuleApi = originalMxModuleApi;
|
||||||
|
});
|
||||||
|
|
||||||
|
function wrapComponent(component: Parameters<typeof render>[0]): ReturnType<typeof render> {
|
||||||
|
return render(component, {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<MatrixClientContext.Provider value={client}>
|
||||||
|
<ScopedRoomContextProvider
|
||||||
|
roomId="!foo:bar"
|
||||||
|
{...getRoomContext(mkRoom(client, "!foo:bar"), { showUrlPreview: true })}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScopedRoomContextProvider>
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("to be empty without a link to preview", () => {
|
||||||
|
const { container } = wrapComponent(<MessageComposerUrlPreviewWrapper content="Test a string" />);
|
||||||
|
expect(container).toMatchInlineSnapshot(`<div />`);
|
||||||
|
});
|
||||||
|
test("to contain a link when there is a URL", async () => {
|
||||||
|
const { getByText } = wrapComponent(<MessageComposerUrlPreviewWrapper content="https://example.org" />);
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(getByText("Example.org")).toBeDefined();
|
||||||
|
},
|
||||||
|
{ timeout: DEBOUNCE_REQUEST_TIMEOUT_MS },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test("to allow overriding with a module component", async () => {
|
||||||
|
const modApi = {
|
||||||
|
customComponents: new CustomComponentsApi(),
|
||||||
|
} as ModuleApi;
|
||||||
|
modApi.customComponents.registerComposerPreview(
|
||||||
|
() => true,
|
||||||
|
() => <strong>Fake preview</strong>,
|
||||||
|
);
|
||||||
|
const { getByText } = wrapComponent(
|
||||||
|
<MessageComposerUrlPreviewWrapper content="https://example.org" moduleApi={modApi} />,
|
||||||
|
);
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(getByText("Fake preview")).toBeDefined();
|
||||||
|
},
|
||||||
|
{ timeout: DEBOUNCE_REQUEST_TIMEOUT_MS },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,36 +1,62 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2026 Element Creations Ltd
|
||||||
Copyright 2015-2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, type ReactNode } from "react";
|
import React, { useEffect, useState, type ReactNode } from "react";
|
||||||
import { MessageComposerUrlPreviewView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
|
import { MessageComposerUrlPreviewView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
import { MessageComposerUrlPreviewViewModel } from "../../../viewmodels/composer/MessageComposerUrlPreviewViewModel";
|
import { MessageComposerUrlPreviewViewModel } from "../../../viewmodels/composer/MessageComposerUrlPreviewViewModel";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext";
|
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext";
|
||||||
import { useDebouncedCallback } from "../../../hooks/spotlight/useDebouncedCallback";
|
import { useDebouncedCallback } from "../../../hooks/spotlight/useDebouncedCallback";
|
||||||
import PlatformPeg from "../../../PlatformPeg";
|
import PlatformPeg from "../../../PlatformPeg";
|
||||||
|
import { ModuleApi } from "../../../modules/Api";
|
||||||
|
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
const DEBOUNCE_REQUEST_TIMEOUT_MS = 500;
|
export const DEBOUNCE_REQUEST_TIMEOUT_MS = 500;
|
||||||
|
|
||||||
export function MessageComposerUrlPreviewWrapper({ content }: { content: string }): ReactNode | null {
|
export function MessageComposerUrlPreviewWrapper({
|
||||||
const { showUrlPreview } = useScopedRoomContext("showUrlPreview");
|
content,
|
||||||
|
moduleApi = ModuleApi.instance,
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
moduleApi?: ModuleApi;
|
||||||
|
}): ReactNode | null {
|
||||||
|
const { showUrlPreview, roomId } = useScopedRoomContext("showUrlPreview", "roomId");
|
||||||
|
const [customComponent, setCustomComponent] = useState<React.JSX.Element | null>(null);
|
||||||
|
|
||||||
|
const client = useMatrixClientContext();
|
||||||
const vm = useCreateAutoDisposedViewModel(
|
const vm = useCreateAutoDisposedViewModel(
|
||||||
() =>
|
() =>
|
||||||
new MessageComposerUrlPreviewViewModel({
|
new MessageComposerUrlPreviewViewModel({
|
||||||
client: MatrixClientPeg.safeGet(),
|
client,
|
||||||
visible: showUrlPreview,
|
visible: showUrlPreview,
|
||||||
showTooltips: PlatformPeg.get()?.needsUrlTooltips() ?? true,
|
showTooltips: PlatformPeg.get()?.needsUrlTooltips() ?? true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void vm.updateUrlPreviewVisible(showUrlPreview);
|
||||||
|
}, [vm, showUrlPreview]);
|
||||||
|
|
||||||
|
// Rather than checking each time the text changes, we only do a URL check every 500ms to avoid
|
||||||
|
// hitting the server too frequently. We also only check the module API for a custom component
|
||||||
|
// at this frequency to avoid expensive calculations downstream.
|
||||||
useDebouncedCallback<[MessageComposerUrlPreviewViewModel, string]>(
|
useDebouncedCallback<[MessageComposerUrlPreviewViewModel, string]>(
|
||||||
true,
|
true,
|
||||||
(vm, content) => {
|
(vm, content) => {
|
||||||
|
const customComponent = moduleApi.customComponents.renderComposerPreview(
|
||||||
|
{ text: content, roomId: roomId! },
|
||||||
|
() => <MessageComposerUrlPreviewView vm={vm} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (customComponent) {
|
||||||
|
setCustomComponent(customComponent);
|
||||||
|
}
|
||||||
|
// We still update the VM even if the custom component is used since
|
||||||
|
// the component may choose to render the original component.
|
||||||
void vm.updateWithText(content);
|
void vm.updateWithText(content);
|
||||||
},
|
},
|
||||||
[vm, content],
|
[vm, content],
|
||||||
@@ -38,9 +64,5 @@ export function MessageComposerUrlPreviewWrapper({ content }: { content: string
|
|||||||
content ? DEBOUNCE_REQUEST_TIMEOUT_MS : 0,
|
content ? DEBOUNCE_REQUEST_TIMEOUT_MS : 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
return customComponent ?? <MessageComposerUrlPreviewView vm={vm} />;
|
||||||
void vm.updateUrlPreviewVisible(showUrlPreview);
|
|
||||||
}, [vm, showUrlPreview]);
|
|
||||||
|
|
||||||
return <MessageComposerUrlPreviewView vm={vm} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,13 @@ import type {
|
|||||||
MatrixEvent as ModuleMatrixEvent,
|
MatrixEvent as ModuleMatrixEvent,
|
||||||
CustomRoomPreviewBarRenderFunction,
|
CustomRoomPreviewBarRenderFunction,
|
||||||
CustomLoginRenderFunction,
|
CustomLoginRenderFunction,
|
||||||
|
CustomComposerPreviewRenderFunction,
|
||||||
|
CustomComposerPreviewComponentProps,
|
||||||
} from "@element-hq/element-web-module-api";
|
} from "@element-hq/element-web-module-api";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
type EventTypeOrFilter = Parameters<ICustomComponentsApi["registerMessageRenderer"]>[0];
|
type EventTypeOrFilter = Parameters<ICustomComponentsApi["registerMessageRenderer"]>[0];
|
||||||
|
type ComposerPreviewFilterFn = Parameters<ICustomComponentsApi["registerComposerPreview"]>[0];
|
||||||
|
|
||||||
type EventRenderer = {
|
type EventRenderer = {
|
||||||
eventTypeOrFilter: EventTypeOrFilter;
|
eventTypeOrFilter: EventTypeOrFilter;
|
||||||
@@ -37,6 +40,11 @@ interface CustomMessageRenderHints extends Omit<ModuleCustomCustomMessageRenderH
|
|||||||
allowDownloadingMedia?: () => Promise<boolean>;
|
allowDownloadingMedia?: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ComposerPreviewRenderer = {
|
||||||
|
filter: ComposerPreviewFilterFn;
|
||||||
|
renderer: CustomComposerPreviewRenderFunction;
|
||||||
|
};
|
||||||
|
|
||||||
export class CustomComponentsApi implements ICustomComponentsApi {
|
export class CustomComponentsApi implements ICustomComponentsApi {
|
||||||
/**
|
/**
|
||||||
* Convert a matrix-js-sdk event into a ModuleMatrixEvent.
|
* Convert a matrix-js-sdk event into a ModuleMatrixEvent.
|
||||||
@@ -66,6 +74,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly registeredMessageRenderers: EventRenderer[] = [];
|
private readonly registeredMessageRenderers: EventRenderer[] = [];
|
||||||
|
private readonly registeredComposerPreviewRenderers: ComposerPreviewRenderer[] = [];
|
||||||
|
|
||||||
public registerMessageRenderer(
|
public registerMessageRenderer(
|
||||||
eventTypeOrFilter: EventTypeOrFilter,
|
eventTypeOrFilter: EventTypeOrFilter,
|
||||||
@@ -80,7 +89,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
|||||||
* @param mxEvent The message event being rendered.
|
* @param mxEvent The message event being rendered.
|
||||||
* @returns The registered renderer.
|
* @returns The registered renderer.
|
||||||
*/
|
*/
|
||||||
private selectRenderer(mxEvent: ModuleMatrixEvent): EventRenderer | undefined {
|
private selectMessageRenderer(mxEvent: ModuleMatrixEvent): EventRenderer | undefined {
|
||||||
return this.registeredMessageRenderers.find((renderer) => {
|
return this.registeredMessageRenderers.find((renderer) => {
|
||||||
if (typeof renderer.eventTypeOrFilter === "string") {
|
if (typeof renderer.eventTypeOrFilter === "string") {
|
||||||
return renderer.eventTypeOrFilter === mxEvent.type;
|
return renderer.eventTypeOrFilter === mxEvent.type;
|
||||||
@@ -106,7 +115,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
|||||||
originalComponent?: (props?: OriginalMessageComponentProps) => React.JSX.Element,
|
originalComponent?: (props?: OriginalMessageComponentProps) => React.JSX.Element,
|
||||||
): React.JSX.Element | null {
|
): React.JSX.Element | null {
|
||||||
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(props.mxEvent);
|
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(props.mxEvent);
|
||||||
const renderer = moduleEv && this.selectRenderer(moduleEv);
|
const renderer = moduleEv && this.selectMessageRenderer(moduleEv);
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
try {
|
try {
|
||||||
return renderer.renderer({ ...props, mxEvent: moduleEv }, originalComponent);
|
return renderer.renderer({ ...props, mxEvent: moduleEv }, originalComponent);
|
||||||
@@ -125,7 +134,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
|||||||
*/
|
*/
|
||||||
public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints | null {
|
public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints | null {
|
||||||
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(mxEvent);
|
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(mxEvent);
|
||||||
const renderer = moduleEv && this.selectRenderer(moduleEv);
|
const renderer = moduleEv && this.selectMessageRenderer(moduleEv);
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
return {
|
return {
|
||||||
...renderer.hints,
|
...renderer.hints,
|
||||||
@@ -171,4 +180,31 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
|||||||
public registerLoginComponent(renderer: CustomLoginRenderFunction): void {
|
public registerLoginComponent(renderer: CustomLoginRenderFunction): void {
|
||||||
this._loginRenderer = renderer;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,11 +146,26 @@ export type Container = "top" | "right" | "center";
|
|||||||
|
|
||||||
// @alpha
|
// @alpha
|
||||||
export interface CustomComponentsApi {
|
export interface CustomComponentsApi {
|
||||||
|
registerComposerPreview(filterFn: (composerText: string, roomId: string) => boolean, renderer: CustomComposerPreviewRenderFunction): void;
|
||||||
registerLoginComponent(renderer: CustomLoginRenderFunction): void;
|
registerLoginComponent(renderer: CustomLoginRenderFunction): void;
|
||||||
registerMessageRenderer(eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints?: CustomMessageRenderHints): void;
|
registerMessageRenderer(eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints?: CustomMessageRenderHints): void;
|
||||||
registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void;
|
registerRoomPreviewBar(renderer: CustomRoomPreviewBarRenderFunction): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @alpha
|
||||||
|
export type CustomComposerPreviewComponentProps = {
|
||||||
|
text: string;
|
||||||
|
roomId: string;
|
||||||
|
target?: ComposerApiTarget;
|
||||||
|
relation?: {
|
||||||
|
inReplyToEventId?: string;
|
||||||
|
relType?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// @alpha
|
||||||
|
export type CustomComposerPreviewRenderFunction = ExtendablePropsRenderFunction<CustomComposerPreviewComponentProps>;
|
||||||
|
|
||||||
// @alpha
|
// @alpha
|
||||||
export interface CustomisationsApi {
|
export interface CustomisationsApi {
|
||||||
registerShouldShowComponent(fn: (this: void, component: UIComponent) => boolean | void): void;
|
registerShouldShowComponent(fn: (this: void, component: UIComponent) => boolean | void): void;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import type { JSX, ReactNode } from "react";
|
import type { JSX, ReactNode } from "react";
|
||||||
import type { MatrixEvent } from "../models/event";
|
import type { MatrixEvent } from "../models/event";
|
||||||
import type { AccountAuthInfo } from "./auth.ts";
|
import type { AccountAuthInfo } from "./auth.ts";
|
||||||
|
import { ComposerApiTarget } from "./composer.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties for all message components.
|
* Properties for all message components.
|
||||||
@@ -157,6 +158,38 @@ export type ExtendablePropsRenderFunction<BaseProps> = <P extends BaseProps>(
|
|||||||
*/
|
*/
|
||||||
export type CustomLoginRenderFunction = ExtendablePropsRenderFunction<CustomLoginComponentProps>;
|
export type CustomLoginRenderFunction = ExtendablePropsRenderFunction<CustomLoginComponentProps>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties for composer preview.
|
||||||
|
* @alpha Subject to change.
|
||||||
|
*/
|
||||||
|
export type CustomComposerPreviewComponentProps = {
|
||||||
|
/**
|
||||||
|
* The plain text currently inside the composer.
|
||||||
|
*/
|
||||||
|
text: string;
|
||||||
|
/**
|
||||||
|
* The ID of the room currently in focus.
|
||||||
|
*/
|
||||||
|
roomId: string;
|
||||||
|
/**
|
||||||
|
* Details about the target of the composer.
|
||||||
|
*/
|
||||||
|
target?: ComposerApiTarget;
|
||||||
|
/**
|
||||||
|
* Details about what relation the user may be responding to.
|
||||||
|
*/
|
||||||
|
relation?: {
|
||||||
|
inReplyToEventId?: string;
|
||||||
|
relType?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function used to render a composer preview.
|
||||||
|
* @alpha Unlikely to change
|
||||||
|
*/
|
||||||
|
export type CustomComposerPreviewRenderFunction = ExtendablePropsRenderFunction<CustomComposerPreviewComponentProps>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API for inserting custom components into Element.
|
* API for inserting custom components into Element.
|
||||||
* @alpha Subject to change.
|
* @alpha Subject to change.
|
||||||
@@ -224,4 +257,29 @@ export interface CustomComponentsApi {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
registerLoginComponent(renderer: CustomLoginRenderFunction): void;
|
registerLoginComponent(renderer: CustomLoginRenderFunction): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a renderer for replacing the preview above the message composer.
|
||||||
|
*
|
||||||
|
* The render function should return a rendered component.
|
||||||
|
*
|
||||||
|
* Multiple render function may be registered, however the first matching result will be used.
|
||||||
|
* If no events match or are registered then the originalComponent is rendered.
|
||||||
|
*
|
||||||
|
* @param filterFn - A filter function to determine if this renderer should be used.
|
||||||
|
* @param renderer - The render function.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* customComponents.registerComposerPreview(
|
||||||
|
* (composerText) => composerText === "hello!",
|
||||||
|
* (props, originalComponent) => {
|
||||||
|
* return <b>You've said hello!</b>;
|
||||||
|
* }
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
registerComposerPreview(
|
||||||
|
filterFn: (composerText: string, roomId: string) => boolean,
|
||||||
|
renderer: CustomComposerPreviewRenderFunction,
|
||||||
|
): void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user