From 213a191b8c3b1e4219f25c1f9aa42cf9c1ed2c1a Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 19 May 2025 09:26:22 +0100 Subject: [PATCH 01/27] Add new custom component api. --- .../context_menus/MessageContextMenu.tsx | 25 +++++-- src/components/views/messages/TextualBody.tsx | 41 +++++++---- src/modules/Api.ts | 4 +- src/modules/customComponentApi.tsx | 69 +++++++++++++++++++ 4 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 src/modules/customComponentApi.tsx diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 76f6b319894..4879907b391 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024-2025 New Vector Ltd. Copyright 2015-2023 The Matrix.org Foundation C.I.C. Copyright 2021, 2022 Šimon Brandner Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> @@ -20,6 +20,7 @@ import { Thread, M_POLL_START, } from "matrix-js-sdk/src/matrix"; +import { CustomComponentTarget } from "@element-hq/element-web-module-api"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; @@ -53,6 +54,7 @@ import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadP import { CardContext } from "../right_panel/context"; import PinningUtils from "../../../utils/PinningUtils"; import PosthogTrackers from "../../../PosthogTrackers.ts"; +import ModuleApi from "../../../modules/Api.ts"; interface IReplyInThreadButton { mxEvent: MatrixEvent; @@ -684,6 +686,22 @@ export default class MessageContextMenu extends React.Component ); } + const root = ( + <> + {nativeItemsList} + {quickItemsList} + {commonItemsList} + {redactItemList} + + ); + const wrapped = ModuleApi.customComponents.render( + CustomComponentTarget.MessageContextMenu, + { + mxEvent, + closeMenu: this.closeMenu, + }, + root, + ); return ( compact={true} data-testid="mx_MessageContextMenu" > - {nativeItemsList} - {quickItemsList} - {commonItemsList} - {redactItemList} + {wrapped} {reactionPicker} diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index d0107b31ecc..90dbef29ed4 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024-2025 New Vector Ltd. Copyright 2015-2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, createRef, type SyntheticEvent, type MouseEvent } from "react"; import { MsgType } from "matrix-js-sdk/src/matrix"; +import { CustomComponentTarget } from "@element-hq/element-web-module-api"; import EventContentBody from "./EventContentBody.tsx"; import { formatDate } from "../../../DateUtils"; @@ -29,7 +30,7 @@ import { options as linkifyOpts } from "../../../linkify-matrix"; import { getParentEventId } from "../../../utils/Reply"; import { EditWysiwygComposer } from "../rooms/wysiwyg_composer"; import { type IEventTileOps } from "../rooms/EventTile"; - +import ModuleApi from "../../../modules/Api.ts"; interface IState { // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. links: string[]; @@ -311,6 +312,7 @@ export default class TextualBody extends React.Component { ); } + const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); const isNotice = content.msgtype === MsgType.Notice; @@ -382,8 +384,10 @@ export default class TextualBody extends React.Component { ); } + let root; + if (isEmote) { - return ( + root = (
@@ -394,28 +398,37 @@ export default class TextualBody extends React.Component { {widgets}
); - } - if (isNotice) { - return ( + } else if (isNotice) { + root = (
{body} {widgets}
); - } - if (isCaption) { - return ( + } else if (isCaption) { + root = (
{body} {widgets}
); + } else { + root = ( +
+ {body} + {widgets} +
+ ); } - return ( -
- {body} - {widgets} -
+ return ModuleApi.customComponents.render( + CustomComponentTarget.TextualBody, + { + mxEvent, + highlights: this.props.highlights, + showUrlPreview: this.props.showUrlPreview, + forExport: this.props.forExport, + }, + root, ); } } diff --git a/src/modules/Api.ts b/src/modules/Api.ts index 23abadf5299..a74a99282ec 100644 --- a/src/modules/Api.ts +++ b/src/modules/Api.ts @@ -6,8 +6,8 @@ Please see LICENSE files in the repository root for full details. */ import { createRoot, type Root } from "react-dom/client"; +import { type Api, type RuntimeModuleConstructor } from "@element-hq/element-web-module-api"; -import type { Api, RuntimeModuleConstructor } from "@element-hq/element-web-module-api"; import { ModuleRunner } from "./ModuleRunner.ts"; import AliasCustomisations from "../customisations/Alias.ts"; import { RoomListCustomisations } from "../customisations/RoomList.ts"; @@ -21,6 +21,7 @@ import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissi import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts"; import { ConfigApi } from "./ConfigApi.ts"; import { I18nApi } from "./I18nApi.ts"; +import { CustomComponentsApi } from "./customComponentApi.tsx"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false; @@ -58,6 +59,7 @@ class ModuleApi implements Api { public readonly config = new ConfigApi(); public readonly i18n = new I18nApi(); + public readonly customComponents = new CustomComponentsApi(); public readonly rootNode = document.getElementById("matrixchat")!; public createRoot(element: Element): Root { diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx new file mode 100644 index 00000000000..1678bb507e0 --- /dev/null +++ b/src/modules/customComponentApi.tsx @@ -0,0 +1,69 @@ +/* +Copyright 2025 New Vector 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 { + type CustomComponentTarget, + type CustomComponentsApi as ICustomComponentsApi, + type CustomComponentProps, + type CustomComponentRenderFunction, + type ContextMenuItem, +} from "@element-hq/element-web-module-api"; +import React, { JSX } from "react"; + +import { + IconizedContextMenuOption, + IconizedContextMenuOptionList, +} from "../components/views/context_menus/IconizedContextMenu"; + +export class CustomComponentsApi implements ICustomComponentsApi { + private registeredRenderers = new Map< + CustomComponentTarget, + CustomComponentRenderFunction[] + >(); + + public register( + target: T, + renderer: CustomComponentRenderFunction, + ): void { + const renderSet = this.registeredRenderers.get(target); + if (renderSet) { + // This type is safe, registeredRenderers maps T => CustomComponentRenderFunction + renderSet.push(renderer as CustomComponentRenderFunction); + } else { + this.registeredRenderers.set(target, [renderer as CustomComponentRenderFunction]); + } + } + + public render( + target: T, + props: CustomComponentProps[T], + originalComponent: React.JSX.Element, + ): React.JSX.Element { + for (const renderer of this.registeredRenderers.get(target) ?? []) { + const component = renderer(props, originalComponent); + if (component) { + return component; + } + } + return originalComponent; + } + + public buildContextMenuBlock(items: ContextMenuItem[]): JSX.Element { + return ( + + {items.map((props) => ( + + ))} + + ); + } +} From 3281a4128ff7ecfa4714bb7f0dac2302aaa5faad Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 20 May 2025 12:10:45 +0100 Subject: [PATCH 02/27] Remove context menu, refactor --- .../context_menus/MessageContextMenu.tsx | 25 +---- src/components/views/messages/TextualBody.tsx | 96 +++++++++---------- src/modules/customComponentApi.tsx | 32 +------ 3 files changed, 56 insertions(+), 97 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 4879907b391..76f6b319894 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024-2025 New Vector Ltd. +Copyright 2024 New Vector Ltd. Copyright 2015-2023 The Matrix.org Foundation C.I.C. Copyright 2021, 2022 Šimon Brandner Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> @@ -20,7 +20,6 @@ import { Thread, M_POLL_START, } from "matrix-js-sdk/src/matrix"; -import { CustomComponentTarget } from "@element-hq/element-web-module-api"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; @@ -54,7 +53,6 @@ import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadP import { CardContext } from "../right_panel/context"; import PinningUtils from "../../../utils/PinningUtils"; import PosthogTrackers from "../../../PosthogTrackers.ts"; -import ModuleApi from "../../../modules/Api.ts"; interface IReplyInThreadButton { mxEvent: MatrixEvent; @@ -686,22 +684,6 @@ export default class MessageContextMenu extends React.Component ); } - const root = ( - <> - {nativeItemsList} - {quickItemsList} - {commonItemsList} - {redactItemList} - - ); - const wrapped = ModuleApi.customComponents.render( - CustomComponentTarget.MessageContextMenu, - { - mxEvent, - closeMenu: this.closeMenu, - }, - root, - ); return ( compact={true} data-testid="mx_MessageContextMenu" > - {wrapped} + {nativeItemsList} + {quickItemsList} + {commonItemsList} + {redactItemList} {reactionPicker} diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 90dbef29ed4..ae82bb98ead 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX, createRef, type SyntheticEvent, type MouseEvent } from "react"; -import { MsgType } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix"; import { CustomComponentTarget } from "@element-hq/element-web-module-api"; import EventContentBody from "./EventContentBody.tsx"; @@ -373,53 +373,6 @@ export default class TextualBody extends React.Component { ); } - let widgets; - if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { - widgets = ( - - ); - } - - let root; - - if (isEmote) { - root = ( -
- *  - - {mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} - -   - {body} - {widgets} -
- ); - } else if (isNotice) { - root = ( -
- {body} - {widgets} -
- ); - } else if (isCaption) { - root = ( -
- {body} - {widgets} -
- ); - } else { - root = ( -
- {body} - {widgets} -
- ); - } return ModuleApi.customComponents.render( CustomComponentTarget.TextualBody, { @@ -428,7 +381,52 @@ export default class TextualBody extends React.Component { showUrlPreview: this.props.showUrlPreview, forExport: this.props.forExport, }, - root, + () => { + let widgets; + if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { + widgets = ( + + ); + } + if (isEmote) { + return ( +
+ *  + + {mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} + +   + {body} + {widgets} +
+ ); + } else if (isNotice) { + return ( +
+ {body} + {widgets} +
+ ); + } else if (isCaption) { + return ( +
+ {body} + {widgets} +
+ ); + } else { + return ( +
+ {body} + {widgets} +
+ ); + } + }, ); } } diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index 1678bb507e0..cfef55e58cb 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -10,14 +10,8 @@ import { type CustomComponentsApi as ICustomComponentsApi, type CustomComponentProps, type CustomComponentRenderFunction, - type ContextMenuItem, } from "@element-hq/element-web-module-api"; -import React, { JSX } from "react"; - -import { - IconizedContextMenuOption, - IconizedContextMenuOptionList, -} from "../components/views/context_menus/IconizedContextMenu"; +import React from "react"; export class CustomComponentsApi implements ICustomComponentsApi { private registeredRenderers = new Map< @@ -25,10 +19,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { CustomComponentRenderFunction[] >(); - public register( - target: T, - renderer: CustomComponentRenderFunction, - ): void { + public register(target: T, renderer: CustomComponentRenderFunction): void { const renderSet = this.registeredRenderers.get(target); if (renderSet) { // This type is safe, registeredRenderers maps T => CustomComponentRenderFunction @@ -41,7 +32,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { public render( target: T, props: CustomComponentProps[T], - originalComponent: React.JSX.Element, + originalComponent: () => React.JSX.Element, ): React.JSX.Element { for (const renderer of this.registeredRenderers.get(target) ?? []) { const component = renderer(props, originalComponent); @@ -49,21 +40,6 @@ export class CustomComponentsApi implements ICustomComponentsApi { return component; } } - return originalComponent; - } - - public buildContextMenuBlock(items: ContextMenuItem[]): JSX.Element { - return ( - - {items.map((props) => ( - - ))} - - ); + return originalComponent(); } } From 4d81b36270d583053cc1dd9bc3c646930d11ccf9 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 20 May 2025 12:40:46 +0100 Subject: [PATCH 03/27] fix types --- src/components/views/messages/TextualBody.tsx | 2 +- src/modules/customComponentApi.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index ae82bb98ead..e4219584079 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX, createRef, type SyntheticEvent, type MouseEvent } from "react"; -import { MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix"; +import { MsgType } from "matrix-js-sdk/src/matrix"; import { CustomComponentTarget } from "@element-hq/element-web-module-api"; import EventContentBody from "./EventContentBody.tsx"; diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index cfef55e58cb..00d3e176975 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -11,7 +11,8 @@ import { type CustomComponentProps, type CustomComponentRenderFunction, } from "@element-hq/element-web-module-api"; -import React from "react"; + +import type React from "react"; export class CustomComponentsApi implements ICustomComponentsApi { private registeredRenderers = new Map< From afab6c29dca1d76d7d6ec85b0e1a138cb4fa678e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 20 May 2025 13:27:22 +0100 Subject: [PATCH 04/27] Add a test for custom modules. --- .../e2e/modules/custom-component.spec.ts | 36 ++++++++++++++++++ .../sample-files/custom-component-module.js | 23 +++++++++++ .../custom-component-tile-linux.png | Bin 0 -> 4128 bytes .../custom-component-tile-original-linux.png | Bin 0 -> 2056 bytes 4 files changed, 59 insertions(+) create mode 100644 playwright/e2e/modules/custom-component.spec.ts create mode 100644 playwright/sample-files/custom-component-module.js create mode 100644 playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png create mode 100644 playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png diff --git a/playwright/e2e/modules/custom-component.spec.ts b/playwright/e2e/modules/custom-component.spec.ts new file mode 100644 index 00000000000..1299e7ff53d --- /dev/null +++ b/playwright/e2e/modules/custom-component.spec.ts @@ -0,0 +1,36 @@ +/* +Copyright 2025 New Vector 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 { test, expect } from "../../element-web-test"; +test.describe("Custom Component Module", () => { + test.use({ + displayName: "Manny", + config: { + modules: ["/modules/custom-component-module.js"], + }, + page: async ({ page }, use) => { + await page.route("/modules/custom-component-module.js", async (route) => { + await route.fulfill({ path: "playwright/sample-files/custom-component-module.js" }); + }); + await use(page); + }, + room: async ({ page, app, user, bot }, use) => { + const roomId = await app.client.createRoom({ name: 'TestRoom' }); + await use({ roomId }); + }, + }); + test("should replace the render method of a textual event", { tag: "@screenshot" }, async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, 'Simple message'); + await expect(await page.getByText("Simple message")).toMatchScreenshot("custom-component-tile.png"); + }); + test("should render the original content of a textual event conditionally", { tag: "@screenshot" }, async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, 'Do not replace me'); + await expect(await page.getByText("Do not replace me")).toMatchScreenshot("custom-component-tile-original.png"); + }); +}); diff --git a/playwright/sample-files/custom-component-module.js b/playwright/sample-files/custom-component-module.js new file mode 100644 index 00000000000..7ef9299c2d6 --- /dev/null +++ b/playwright/sample-files/custom-component-module.js @@ -0,0 +1,23 @@ +/* +Copyright 2025 New Vector 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. +*/ + +export default class CustomComponentModule { + static moduleApiVersion = "^1.0.0"; + constructor(api) { + this.api = api; + this.api.customComponents.register("TextualBody", (props, originalComponent) => { + const body = props.mxEvent.getContent().body; + if (body === "Do not replace me") { + return originalComponent(); + } + return `Custom text for ${body}`; + }) + } + async load() { + + } +} diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..ed626c902a1972a8c65cea633abe6f492521b80a GIT binary patch literal 4128 zcmai%_fwPK(}o{{0#XD4>7WQo?+O@^NDW1bAiWxq-XR1DQNU0|kS@}si1gl@^deP4 z=m?<`dMDH`pLzd)_m|x>d+(h4%6PAerhKtS|DKXin)Coaw?GzYqxcnaW0;1zC}H>HLECz?jyR!t)b575Yy zaiT@JqPV2CsJJ*HtI0AoM#?IK66s! zktHByqlaMm1$*ULhS&P1t>}gE+P)^Tm zj&`nH%U*jxz;iJQ)lFOp)~G+6*>8cLmK-HxS}Pf|!Fp}WGKeY=nw>e#XCEZ44P7;#o@zL#L` zirV`t1IicD>ZwtAlm=#ojMY&J$V;~CJL+0l#h<3C9HedxpI=4fUS+p*USNmj0BYE9 ziYU9DjpysW`ggnKg(EYaNy<+@S$#?xQD)_~LsnrPiJ7Fn!rF&l}A-xkZ27;yVLF zpg%coR60(jde;iPA<;Len}mL!6mPJAIrbVZSBh(OfdF->rH&%g#U*?vqp(g+M3vYh z6lxgO6Cs&h?UhMwk*wYOjmWUBa>OSL$7jSG=Tk6I*VckpGZ>6{6c?Xe6i1$R80|9P zgdfqff%c$Iia;WS&gs#5FY$0E=H6u&UyDIgl>{;wE=@!^5R{L1)cZQi*0j#H_6I$E zs>ub2!udWtu+w3u0xTt~tohNOVfjesh&xj%gX)CVO1!XgOx*|u%Tv|lGF{S=(_@aS zbT@FuPZE4n6Y`WHU}X@{SCD?ywfS&Zr1WG{(ZapvO=?7CYOP?H5IFvB>BwgYlqJe2 zf75ZmfRB4{&um47OPsav?O!}^PsVu0UO~uDb6@{o7Ao=|%#N9+6u4FtWwdt7N=Du~ z^=a|XTsfAboVp}3^o934?|KsoHF)bp8#p^E?moOpAncgU$aDx~lx^z`97!^6lE<7~ zS5D4zHdFQMSL<=J(d@lfEtF70aX!$jXN?|e1Km| ziyTwUA;F+;=~#<_?QCZf!qJKSB*wfd{CqdLJ*L50= zE%ADjHE1@3u z^YYC-dVe`b-lOwrl zbFQJAPDNXbE~sBs){%WVn%c^X;uILHMr8-CXmW3Qp$%%?`LA;$+BwaMCuuOQV+R=) zJzr+B1=f(LVo-MJ#<(W5FVSy&K3=Hc2-k^Q?PNOrV2yl%km2KBkP!@-Hb{%(jL_c2GEred-aep3~ngHL#Fj}~m(h=Y$v13&BlXK11M%4N`5oe9uhs!-g z)A{ic^%GBlBP5r@BNb@9>fhEq>XiON#3$XUXe(;%gNDB1#8Qsukr5QW(>NY|hBwpBTkA9*z)eOq6#(H0(LletFx8#mo&5kl(m zWt07QumBcr_cHxC5h#DXNPjh_oXsz30yJe3W1bQ@tBxMwielgqJc*>G#e4r~5MU^b za`rJ)G%J*p_dX7tLiI_mVDjvZll9%LQVv(ugmJ5%!tj%@R7Q#L&}ZtUn%5RIS#bnt zxv34PS=y`ii!@<`N30nK27nT7-5JACxpMyj&x^DE`&xV_3ZhDYUu(i+-s(z!c<<(h zl5=}HLO08l70^D}pTHJlCE=tL=okp}zX67-T5(Hi4H~C*pOsx+#IJ*vJlIMuzARue z%_Ti0u?ICb8upN9+e}5heTiCwbDAFIS0_^~AD>4fE6~q_)k``*V7R}NFaGt1-^zUHuN?WiGlvc2T5Igg>hjvtPv1RkFkIj-aUE_M^?6TSRf5yEHp!md4+<{hBx4mVRS0Fr z@gCf{?b0T#EepfH_>{el44lf*epa_899LodB+mo-iRIDFN2Jyb>S^be>8+5Zzg+_kWoI{3-fHP^UU^X=|NwO0q z1AtEu-)FWGZT}bm%N^?}B;)TC*2V23Zs2ROzkpPDM&j%_opja(*QsG-ruJfAs@<8u ze%i?DCsqX;rw*kU7uOQYfU1GviZCshXa8X(KJ4vU*FHHhMV@=?V&<&_9%o|UE5*XF zq^dG>01m%r++;XvMN-pWR&BI?Gtz=-OrdT-fn^n1Xl|ibNWplE2itp)hIS@lLWLk$ zo1@4lQfXIZ`H2#@xWDbRJINF=Q-5~H) zj^}nR*SZYZmf?~^NT(UAS8{nwkl>xOqG*yoQ@1yY2x6CRB*ANFUh(HfvS5GiZq<)l zw>Z%sG&rO+M5j-BksU7E{rQA*gom~4yGE#c!Nyx_8i3>sayqBb-!J0uCt?FKI!r7q z;thFE@KkN-kP~rXoH|?e6=$2iSd!0vBvJ1?!eeiAI*pr|fJ+&%0s;4%tyZNdA%I5Y zbOh57#wR{m$Rt63bZ55G&g08w|Hmic1ya>SKuxTnm6k(sglVOfiz#-xP8Wa%3~@6Z7NU1Ur()t>U=>%I3_gZ=R{&Du+Q1q`Ia?q=|C zl`^St)TuVQ=QJ2MhgU8$W9jei6HlvxAC?2aZC@g#5r5mDfJaE_0_B5ewyIU&xMdl@Tt?}rc@oSAVIR<$F?-$6HTB^V=jfd z8hJ)iTrUIp=4t(uiG6-?oeS#OWD+qTA5|_3d1eOS{@uV#dY8>K*AW3sm0d7{dp5*$ zx-bUtOpR;VDY!RfU2&YIdC9NzS5mg!oX)^7JZe6qwVg7=WLcA`j&+}+UdfG51WbGY z@h*|Pk?_`L=xBO1(_g_S_z}rt9`1*7zMkKT7y9uHu^S9yi@NW`28V3KJ(9Y2)PBAe z6(|7nODPBmhtku&ph2`te(({K8>d65Q*_zrjemx(<&o*v%dM@y6isnj;m4A=!R7=9 zo@0K9-LZ7+pNev#Jce7>vyf9)&|C&*fk3NKGa}tWs@#U$U{X`@GTR6U}?LOx6vPCGL0jVRy zh&QscS~e=4GGG{~T+jn<>E72?`%C@r z6PRK;JPWM~6OIL-#fC_Z%gQ!UWyefk54+%zv$(fo!RlSNreLe%5OLzSVrIqDMHN8@ z1)kVbvu)Fc-I*DxLTVGKgOa{9bBm3qdnG{;7b1gEOP-VlCLa%8Tc3bZDGm!f50@LL z>t1GNJJgUWoIOkW$>q|1kH47t11hFsT5mB|nQOE&17iP)Px+!bwCyRCt{2-EBx)SsDlMXWGCCBjmDy1PWg1OcJ&=nHE#mHVw{Nafx+zV%@E= zt&Q68ZPBq^Z4@1~O*chd>*`di)S9+V5|rAxb;?dUu|{2<|+^E`)q_;b#^khh*ce+~#Cgb)gwU2nPpgb+e#hr_Hz z2qA>R3$qp>gb)fZ%vyvHLMXg2YY{>Sq43%Te!lm^=*TEV@K1B(M1i&YYo5O8&i)5{ zT;m5ZC$7)$guNhy5ZbnO0RV^ybI{!OX;0r@I@&Iplzyiz(~!|PDfq)(o${ZZ)xKZ) zr4a5y2qA>f8xy=kCNf3rQJp^Ckan)D>CV8=C9SKgvT>Au)9oRI5JKDDmd|S?n$uN< z2Yus1&frzD!1daSPP`wj*ncEByR>!0z4=y_&f7B%8)`o5Z!AsM?N=xc8%yqT-r&iY zx<=Y9*#_-?g*q|4pw2NP0Qfy$T7IdkbP7KV7sM*GrNirv(ShsV>gJlvBZ_xpN`^e( z_qiT4TQUt=l|rpI7S#362x~^xIXWxTbsB}@u%Y0xV@BB2px-xlzrN&Tf;vhO85NUp zxqohPweuIdi%I^ls&!~S*mkIjYXz#?9IY0kc7L??WNEk4J=1T^`c+J1RJ86yQ8(uc zj$Go$Z&qb}a76KLwD$Pfn!C=;JtBk<+L1!M8x_IowJhZq1PZX^?k>-*o}fqb8`_%6 zQuZu#mgd>UJX_52xd;0nN1bjS`f9l8cVw!us+02pfG}n)D7CvIjTKEDSIt@&tIsO8 zyQnOs>2J;koqVtH*7O3`Vw8*4ujcnnk2)o1Zck1P*B+AzWA#~oaJX4x#V0Mb=d{Zs z*8CcWi>{8eH$Mbp(-)INSBx-b&9Cfttxqz3xH7+bh{l(-_4V|%@p( z)!lVy61e4YBX;lIv0sv#8f@YKtkGoAO765?60hHd9fDz+m?|8DwawJ0QlAP(ST)^6Q~xxTfe)b+V`{*UO55EkB6$L z-nea5QeJj~k`XC$^3orE?NFLbi9eSCm@b${$8U0f_nO%i=imrUX>hcqvH$?TPIzzk z*{lb{b6M&`lGpj#5JCuTBcZaE(nTLcNEk5{<~c#4j#aK^0+DOtlyLW3m)FF~H!Q|5 zQYQGnm@ov~OBB3Mr`aMjT zWV}Fu1puNtpkpI~pUU^?G-PCY-V5)n9ZcZaimzk^K+2xFSe2$+Gkv#AC4HU65JCuTFI#1;-#6o^@0^t!&(q08gs8Q0 zpL5#nOAOqyQep0q8Al zWi-Fg1s?0tXWSGoi+UU2p8!BzoYS?W)kFXQ2;XyaUPjN#39P=CMER*PjyZGtR;`W! z0KaFjAHW~T&h@+VduB!_ynB-q_lBco)>Rm8F{B3iFw1D^=9ldcH4{N z&Yw<>TAa9UvkvYy-@8+#5A47_V5=k9$!f93+53s($-d&IgKJh3N#s)Ugl;lL?*i)-UduRiX2ni7RVsB9>}v zMwbXWVaes4M?fqx%sfv{;GUvXWotI0^H z0hvC(_|$J6U;AOx?nDyR0d-1kb9ub%Wffls(U@CqK&7RzsP~TmM5aGe-*yp$79oUw z65iVOzZLJ}8Z&dhN$Tmlu<`w1gb+gL#{hqO03n1B3NOrBgb+d~yte-xGC~L;^y7x- mT7(cnD7-Lh5kd%|@cI`t2}ap;;TM1a0000 Date: Tue, 20 May 2025 14:27:27 +0100 Subject: [PATCH 05/27] tidy --- .../e2e/modules/custom-component.spec.ts | 22 ++++++++++++------- .../sample-files/custom-component-module.js | 6 ++--- yarn.lock | 5 +++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/playwright/e2e/modules/custom-component.spec.ts b/playwright/e2e/modules/custom-component.spec.ts index 1299e7ff53d..e0741f18cec 100644 --- a/playwright/e2e/modules/custom-component.spec.ts +++ b/playwright/e2e/modules/custom-component.spec.ts @@ -19,18 +19,24 @@ test.describe("Custom Component Module", () => { await use(page); }, room: async ({ page, app, user, bot }, use) => { - const roomId = await app.client.createRoom({ name: 'TestRoom' }); + const roomId = await app.client.createRoom({ name: "TestRoom" }); await use({ roomId }); }, }); - test("should replace the render method of a textual event", { tag: "@screenshot" }, async ({ page, room, app }) => { + test("should replace the render method of a textual event", { tag: "@screenshot" }, async ({ page, room, app }) => { await app.viewRoomById(room.roomId); - await app.client.sendMessage(room.roomId, 'Simple message'); + await app.client.sendMessage(room.roomId, "Simple message"); await expect(await page.getByText("Simple message")).toMatchScreenshot("custom-component-tile.png"); }); - test("should render the original content of a textual event conditionally", { tag: "@screenshot" }, async ({ page, room, app }) => { - await app.viewRoomById(room.roomId); - await app.client.sendMessage(room.roomId, 'Do not replace me'); - await expect(await page.getByText("Do not replace me")).toMatchScreenshot("custom-component-tile-original.png"); - }); + test( + "should render the original content of a textual event conditionally", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Do not replace me"); + await expect(await page.getByText("Do not replace me")).toMatchScreenshot( + "custom-component-tile-original.png", + ); + }, + ); }); diff --git a/playwright/sample-files/custom-component-module.js b/playwright/sample-files/custom-component-module.js index 7ef9299c2d6..14cd3788384 100644 --- a/playwright/sample-files/custom-component-module.js +++ b/playwright/sample-files/custom-component-module.js @@ -15,9 +15,7 @@ export default class CustomComponentModule { return originalComponent(); } return `Custom text for ${body}`; - }) - } - async load() { - + }); } + async load() {} } diff --git a/yarn.lock b/yarn.lock index c159963cd72..0af946ad788 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3765,15 +3765,16 @@ classnames "^2.5.1" vaul "^1.0.0" -"@vector-im/matrix-wysiwyg-wasm@link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm": +"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm": version "0.0.0" + uid "" "@vector-im/matrix-wysiwyg@2.38.3": version "2.38.3" resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.38.3.tgz#cc54d8b3e9472bcd8e622126ba364ee31952cd8a" integrity sha512-fqo8P55Vc/t0vxpFar9RDJN5gKEjJmzrLo+O4piDbFda6VrRoqrWAtiu0Au0g6B4hRDPKIuFupk8v9Ja7q8Hvg== dependencies: - "@vector-im/matrix-wysiwyg-wasm" "link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm" + "@vector-im/matrix-wysiwyg-wasm" "link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.3-cc54d8b3e9472bcd8e622126ba364ee31952cd8a-integrity/node_modules/bindings/wysiwyg-wasm" "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" From 1b2d9b392c61961d0fcca48133df917c3ec253e7 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 3 Jun 2025 13:42:17 +0100 Subject: [PATCH 06/27] Rewrite for new API --- src/components/views/messages/TextualBody.tsx | 105 +++++++------- src/events/EventTileFactory.tsx | 132 ++++++++++++------ src/modules/customComponentApi.tsx | 51 ++++--- 3 files changed, 161 insertions(+), 127 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index e4219584079..d0107b31ecc 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024-2025 New Vector Ltd. +Copyright 2024 New Vector Ltd. Copyright 2015-2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, createRef, type SyntheticEvent, type MouseEvent } from "react"; import { MsgType } from "matrix-js-sdk/src/matrix"; -import { CustomComponentTarget } from "@element-hq/element-web-module-api"; import EventContentBody from "./EventContentBody.tsx"; import { formatDate } from "../../../DateUtils"; @@ -30,7 +29,7 @@ import { options as linkifyOpts } from "../../../linkify-matrix"; import { getParentEventId } from "../../../utils/Reply"; import { EditWysiwygComposer } from "../rooms/wysiwyg_composer"; import { type IEventTileOps } from "../rooms/EventTile"; -import ModuleApi from "../../../modules/Api.ts"; + interface IState { // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. links: string[]; @@ -312,7 +311,6 @@ export default class TextualBody extends React.Component { ); } - const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); const isNotice = content.msgtype === MsgType.Notice; @@ -373,60 +371,51 @@ export default class TextualBody extends React.Component { ); } - return ModuleApi.customComponents.render( - CustomComponentTarget.TextualBody, - { - mxEvent, - highlights: this.props.highlights, - showUrlPreview: this.props.showUrlPreview, - forExport: this.props.forExport, - }, - () => { - let widgets; - if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { - widgets = ( - - ); - } - if (isEmote) { - return ( -
- *  - - {mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} - -   - {body} - {widgets} -
- ); - } else if (isNotice) { - return ( -
- {body} - {widgets} -
- ); - } else if (isCaption) { - return ( -
- {body} - {widgets} -
- ); - } else { - return ( -
- {body} - {widgets} -
- ); - } - }, + let widgets; + if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { + widgets = ( + + ); + } + + if (isEmote) { + return ( +
+ *  + + {mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} + +   + {body} + {widgets} +
+ ); + } + if (isNotice) { + return ( +
+ {body} + {widgets} +
+ ); + } + if (isCaption) { + return ( +
+ {body} + {widgets} +
+ ); + } + return ( +
+ {body} + {widgets} +
); } } diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index f1fc224471c..48d2090c4d7 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -43,6 +43,7 @@ import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline"; import { ElementCall } from "../models/Call"; import { type IBodyProps } from "../components/views/messages/IBodyProps"; +import ModuleApi from "../modules/Api"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps @@ -257,7 +258,16 @@ export function renderTile( cli = cli ?? MatrixClientPeg.safeGet(); // because param defaults don't do the correct thing const factory = pickFactory(props.mxEvent, cli, showHiddenEvents); - if (!factory) return undefined; + if (!factory) + return ModuleApi.customComponents.renderMessage( + { + mxEvent: props.mxEvent, + forExport: props.forExport, + highlights: props.highlights, + showUrlPreview: props.showUrlPreview, + }, + undefined, + ); // Note that we split off the ones we actually care about here just to be sure that we're // not going to accidentally send things we shouldn't from lazy callers. Eg: EventTile's @@ -285,35 +295,53 @@ export function renderTile( case TimelineRenderingType.Notification: case TimelineRenderingType.Thread: // We only want a subset of props, so we don't end up causing issues for downstream components. - return factory(props.ref, { - mxEvent, - highlights, - highlightLink, - showUrlPreview, - editState, - replacingEventId, - getRelationsForEvent, - isSeeingThroughMessageHiddenForModeration, - permalinkCreator, - inhibitInteraction, - }); + return ModuleApi.customComponents.renderMessage( + { + mxEvent: props.mxEvent, + forExport: props.forExport, + highlights: props.highlights, + showUrlPreview: props.showUrlPreview, + }, + () => + factory(props.ref, { + mxEvent, + highlights, + highlightLink, + showUrlPreview, + editState, + replacingEventId, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + permalinkCreator, + inhibitInteraction, + }), + ); default: - // NEARLY ALL THE OPTIONS! - return factory(ref, { - mxEvent, - forExport, - replacingEventId, - editState, - highlights, - highlightLink, - showUrlPreview, - permalinkCreator, - callEventGrouper, - getRelationsForEvent, - isSeeingThroughMessageHiddenForModeration, - timestamp, - inhibitInteraction, - }); + return ModuleApi.customComponents.renderMessage( + { + mxEvent: props.mxEvent, + forExport: props.forExport, + highlights: props.highlights, + showUrlPreview: props.showUrlPreview, + }, + () => + factory(ref, { + // NEARLY ALL THE OPTIONS! + mxEvent, + forExport, + replacingEventId, + editState, + highlights, + highlightLink, + showUrlPreview, + permalinkCreator, + callEventGrouper, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + timestamp, + inhibitInteraction, + }), + ); } } @@ -332,7 +360,16 @@ export function renderReplyTile( cli = cli ?? MatrixClientPeg.safeGet(); // because param defaults don't do the correct thing const factory = pickFactory(props.mxEvent, cli, showHiddenEvents); - if (!factory) return undefined; + if (!factory) + return ModuleApi.customComponents.renderMessage( + { + mxEvent: props.mxEvent, + forExport: props.forExport, + highlights: props.highlights, + showUrlPreview: props.showUrlPreview, + }, + undefined, + ); // See renderTile() for why we split off so much const { @@ -350,19 +387,28 @@ export function renderReplyTile( permalinkCreator, } = props; - return factory(ref, { - mxEvent, - highlights, - highlightLink, - showUrlPreview, - overrideBodyTypes, - overrideEventTypes, - replacingEventId, - maxImageHeight, - getRelationsForEvent, - isSeeingThroughMessageHiddenForModeration, - permalinkCreator, - }); + return ModuleApi.customComponents.renderMessage( + { + mxEvent: props.mxEvent, + forExport: props.forExport, + highlights: props.highlights, + showUrlPreview: props.showUrlPreview, + }, + () => + factory(ref, { + mxEvent, + highlights, + highlightLink, + showUrlPreview, + overrideBodyTypes, + overrideEventTypes, + replacingEventId, + maxImageHeight, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + permalinkCreator, + }), + ); } // XXX: this'll eventually be dynamic based on the fields once we have extensible event types diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index 00d3e176975..383a97bf6a1 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -5,42 +5,41 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { - type CustomComponentTarget, - type CustomComponentsApi as ICustomComponentsApi, - type CustomComponentProps, - type CustomComponentRenderFunction, +import type { + CustomComponentsApi as ICustomComponentsApi, + CustomMessageRenderFunction, + CustomMessageComponentProps, } from "@element-hq/element-web-module-api"; - import type React from "react"; export class CustomComponentsApi implements ICustomComponentsApi { - private registeredRenderers = new Map< - CustomComponentTarget, - CustomComponentRenderFunction[] - >(); + private readonly registeredMessageRenderers: { + eventType: string | RegExp; + renderer: CustomMessageRenderFunction; + }[] = []; - public register(target: T, renderer: CustomComponentRenderFunction): void { - const renderSet = this.registeredRenderers.get(target); - if (renderSet) { - // This type is safe, registeredRenderers maps T => CustomComponentRenderFunction - renderSet.push(renderer as CustomComponentRenderFunction); - } else { - this.registeredRenderers.set(target, [renderer as CustomComponentRenderFunction]); - } + public registerMessageRenderer(eventType: string | RegExp, renderer: CustomMessageRenderFunction): void { + this.registeredMessageRenderers.push({ eventType, renderer }); } - public render( - target: T, - props: CustomComponentProps[T], - originalComponent: () => React.JSX.Element, - ): React.JSX.Element { - for (const renderer of this.registeredRenderers.get(target) ?? []) { - const component = renderer(props, originalComponent); + /** + * Render the component for a message event. + * @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 exists, or originalComponent returns a value. Otherwise null. + */ + public renderMessage( + props: CustomMessageComponentProps, + originalComponent?: () => React.JSX.Element, + ): React.JSX.Element | null { + for (const renderer of this.registeredMessageRenderers.filter((e) => + props.mxEvent.getType().match(e.eventType), + ) ?? []) { + const component = renderer.renderer(props, originalComponent); if (component) { return component; } } - return originalComponent(); + return originalComponent?.() || null; } } From 757e4e1395fa1c84510d61b8911f04ba3f5c9cb0 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 3 Jun 2025 13:57:57 +0100 Subject: [PATCH 07/27] Update tests --- .../e2e/modules/custom-component.spec.ts | 5 +++++ .../sample-files/custom-component-module.js | 12 +++++++++++- ...stom-component-tile-fall-through-linux.png | Bin 0 -> 3803 bytes .../custom-component-tile-linux.png | Bin 4128 -> 4098 bytes 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png diff --git a/playwright/e2e/modules/custom-component.spec.ts b/playwright/e2e/modules/custom-component.spec.ts index e0741f18cec..0a48c800c7c 100644 --- a/playwright/e2e/modules/custom-component.spec.ts +++ b/playwright/e2e/modules/custom-component.spec.ts @@ -28,6 +28,11 @@ test.describe("Custom Component Module", () => { await app.client.sendMessage(room.roomId, "Simple message"); await expect(await page.getByText("Simple message")).toMatchScreenshot("custom-component-tile.png"); }); + test("should fall through if one module does not render a component", { tag: "@screenshot" }, async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Fall through here"); + await expect(await page.getByText("Fall through here")).toMatchScreenshot("custom-component-tile-fall-through.png"); + }); test( "should render the original content of a textual event conditionally", { tag: "@screenshot" }, diff --git a/playwright/sample-files/custom-component-module.js b/playwright/sample-files/custom-component-module.js index 14cd3788384..62cfe883daf 100644 --- a/playwright/sample-files/custom-component-module.js +++ b/playwright/sample-files/custom-component-module.js @@ -9,13 +9,23 @@ export default class CustomComponentModule { static moduleApiVersion = "^1.0.0"; constructor(api) { this.api = api; - this.api.customComponents.register("TextualBody", (props, originalComponent) => { + this.api.customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => { const body = props.mxEvent.getContent().body; if (body === "Do not replace me") { return originalComponent(); } + else if (body === "Fall through here"){ + return null; + } return `Custom text for ${body}`; }); + this.api.customComponents.registerMessageRenderer(/m\.room\.message/, (props) => { + const body = props.mxEvent.getContent().body; + if (body !== "Fall through here") { + return null; + } + return `Fallthrough text for ${body}`; + }); } async load() {} } diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..952b0110b6526bed692a63031cc063ba9fd7165c GIT binary patch literal 3803 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 Ruqyxn002ovPDHLkV1k8JUu6IQ literal 0 HcmV?d00001 diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png index ed626c902a1972a8c65cea633abe6f492521b80a..410365982b864a0a97d16e2a78a4acf3421ef15a 100644 GIT binary patch delta 4087 zcmVF0PwFG)lP%0`FML`izL=<_b1*tq*DO^EVw!mnS-VUU-hF)x7YKfy` zXc?3G$Z2BF{ewqA0hQ`-ul;_DKf*qH@AI9Vu=cmkK0CZX(|e0>yvY)S!LpfdZKN8#XNxhm>ikskNUhHtI@Q+M#yy zWvPXw`4K|q6eH9t55DaaQ>76si_gp35#h#G9SB^V(SNEn0WfVvLg9RSTNY!AeItYr zLKAt6x#jlt&l+ma^^4ZO?ZzbUmq4k?lG9q)N#!SF7Yt6rKK|hwD&l<7(u-Y7<6WjY zHUGb37=O5?WANV!@bk7Orre3Pv-(sQ8S#Eetm9xOJ|6bWZB~P2V^E{4@RY4`052;{5l92kPGapR*q}eQz97cK`0z zAD7j1-+knXrbu^jr`Oy?ag8^}p7vIEZNZVs%YQ%8233)ti~H8|x>5C=CSZG6X2Y;O7YHu28+R*n|(PqW+glX$6ERf zG)>b6RgvEU_k^ONtpV;1bDU)Bvp?-to!%QFbK%YR8Q`3=W`Y1Y6DH* z`hTi&Z}gi|N3M;dyF4ztzFSAr^vyGyr9$_FGkr8o(?;EAyH_~4MV{`_j1${9J#%eb zBI~bD7&fiz+`HUKz9oBWz(VfJPSGE0$BbL^=0(ESb~bD2hYdD7pfb>OUuB51TV%np zbcLrA*U??EyY_N-OK$XvMRT||j_!)xjempZ>Z=S{_ny&D4;_~N8SI6a^Q4dDZ zR~h2GII{jaP17`eyYJH@n-$9!a$lb7?6r35v5Pk!{AfMFXdA9yJiaqT?l#wk?<5U~ zD>yfF-So|i1qp9TUAQ*nT<7H>dmmM9_^zd3%UYQW*M{VBJtK2R>Kk>JKT3~Y>3{C< zGS}HFGPnGYV)4T0GupvJ-=4`%2zbqz&vjV5B7Apc$IS;X1R;dbxE{S~V`j}0^Zw0Z zq~Z*QnHk)@@pm0D+hPX5FF#$bIZ^WYfDr(GG2I>bds|IK$w$q~8~29l-~Rr}E%qyR zvz}E-rPHYUiB}{J?6J3dSoS^<>wo=3R+BZ=ev-4bK*^NvXguFk7|c@_Cug>Tch+Cp z_ej_;`4o2aD);-a8T!scQ9%ckEYGN{1A8_uVRmG1h&iOx4R)c|oc*NVC8?~jJXv(L zIOvVg3X?pmrnPae-298;eN|m!7H61}l(U}~uC!)*rnalUDGlJm)v9;iPJgZ#5cv~HaTce+j_hNhi^#ld?Dv*LX@y$55$GB4=Cq;ET# zoY@K*V8$%FS3TBP&9S@FeyYCxMTLhQ0e7$U{=npMroN2u z2-ICYWg^&C=2K{@(|`6;))M~!9}x#2cu%FL7TDGRV<(Us?WgKB^AoDGg82jhfXs#2 zzq+ifEkL{!00^eXnm}Ji5`eu?(R4Nkd=)+-5`Yu+_Jzi6N~M+(k`rD#r9M-AnT#qr zGPJbhEg^OO=^tOvc>9sw(a9IT*nBX**tNUX;I$)Z*~Hz9Yk%Ufy)6NN5Xhn;WWsTt z2w{q2-~Wq8eeu!a{GdN)0Eh446}WM|pUXpUAz>x<50kr)1aQLMZmoSsBYR6OCJ7*e zqdGs?(0NHixd{OP7}ma9_QZNw1AzaQ9K*F$M^Cp#NPI?zTGL2*?&THhY=s#vQvu;%%!lZ#0L1SusR zVHAr)0O-drXNZ%^KaT(=%Mt*9Vry%fN;)-^FGthxQ7d>Q=e_4g5&+=i%GSx_+5rG4 zP4np%R?zc`rr~W64XJhfqo}eWxm@I;?z;mIW+aauZOYKqrM{}% z_GgD}quLRRHM$pM9tuTGOon`UwSX^lagi)tCSx-IzvA5wMj20J`3eu6|WxsIX{s(AF9)1(=Z_gwUfrc}8;YC*6$M zEPpdI#tbk50D$q{Pe#UU3o`&P{hgOkzq%B99Qt^32S8GFERmJht6S?`mdDU5R_A`ujwEF-6 zY`3VC^^(z%vuyc8(-%bOKW94S!P_%U-(9Uh2DqTpdzf)1?pzr_{^Lhe9_2 zjNU(dAHa(APe}IVk4ivhS_>v!ahnKb0&`NDl%f!^em$)@lW#`=u^_7Syr1e~W5em{ zik(#l3#7Zs53jQ)SmTwiA)2;^Z$4`-_4gFJu9W(EweQkR{*4OQ@|)&#WJ{qn6T~J+M8q{u;#qAP52sx&htr1!y!Q zPehEgn>L2b()1lPBfAH9&J`(1(Jb|S_Z|N{J2!9fZz=8rR zHhU`PB-ngD+oT6_ct~FrvP~3W%_IOQtwwW)DO#dP%ByQXpWy+@=1vVYL3#hB#^!&D zQ}XtwYz*`Dl+Gs!!{7(Q($}U-A@rPiyn7^n6s=JVy?^HE`Nk(lxLNmoaBS^rPxdV>%=s$=m)57o1W~GRxhsdzUTHsD zS)~)l`_5xNG7TCR0e||o+|8MKg)4_rH|FoVKzQtz3JB1QR3PQCVNKzCqY?+A*2x?| zc`_}nSSveRxkJW&@>_Upw+*q26={i^sOT_XF;iWYkyy~>x}!QvPMzAkYTpfMY*Li( z%l*w|)tx%xbvBEjdQOa2KHHLH>B=fTXc7fGgR-gOgDRCCM1KHa$$#BXdSY`%BF8)> zz!@%`Ov`T4nwS6pmVEz>isqQ@F&Wy}HC}cDExBo_H8T~Z$C6#AbiW57gnnP9d$Z3| zV$^nglpVd&-GSuVkaMNM>8Dk?k%PCsDvkE!+K^mldF0;8%ALboSn1p9`rT_6&EeXR zTpRvEzb&Pge}ANDnx=J~1>p;ANUlRb>9+=&HtITxBBlJnYVOg;I&T^2q5Y_%_7x9q zXFWZ*oz+JGo!B>Ml5C7V!8z;6=Zk^vOb$Ro6_i*V1VUOO#JAW=icV&WnFr>NCl_6V?we;P7 zu;G}!yGHG&@47pTT&%8h?{X(uR_AcFkb?4b#bT}vIaj(ma{S%DMqS6TNa@HM@m}R+ z-9u45{%Gser?#wc7~BZGU`4{n`*pn~cl~Q4<*-qNqDS3_sONT`3M)ds48kZr2igUIZb8rtZ1; z7I}pJbtv`mP>FkBT5XSx(rdn`%1tlsU`jmZU~oEu5JJBR{)ZBTo(LgWnY!yveqp*y z7y|%6x`pKx#(O+hH-QjBXd2+E4MGSZgr*;^+8~4wLTLJ7MuHGR2%+hR83{rNA%vzM pW+Vt9gb88Buc8t+`^HQC}FN&jKHYnhY&%S|bs?$$aC> zG=R<*oS8n$Zd|T6E4NvYw=!WEw%h2h!W2IA_DXT>^By`5&jnANX0BbtnwXbWUMP|M zi03?n=GasZAdUP!gZJ`=Z*}}vlw?Y(dD-u3a$HD zq586=?zftN`s<59-@vPy0ORR5zshxm)n-HrvhuQZSE?0!7eEy$cc__#5`;#gI>iCw5 zbs^>G+F>k?yRy+~s<=73+ib4F)xdFm0`Hv?pQ{W8DH8&{`DxdkTaSl?OHQ{G&0VYC zri53d)ChzMg5n;O41WScm?Dkxw(R>2crjc9`_L5?PBG?!cYoG-y3@zf_w$2)n0fpD zG*^-T06k%tRN!1ul-AlSEgpX7(5uBaeQjTccIXsO*B9D%f8dEP(BP>Rspn`ffAIJ= z9={8nmTu=yFWu4WKbD~1B8xu1>6~8tD8X|c^(k+S3*h*5I3k42=n^rtA}z>9Os=LA zIwwy^(x%{*z$`iX0kb%cH3IhYE{8uG#cQ0JC-AK8iA~3d!sxwMEyv z)dg*&1&`ATxF%)ux^W;!U*E+*YJSe?ymR6DmG|^<9V4OZ)l{XHyle)K=Pl%fu#)e&2#sG~QS}a8By1bI6uM;r5~v`e!9PqBmPpTX|889E;PaXrC*b z*q>Txg}^)hc5FsDraEA_69;13cTu5H^Q9)+APwQr=5XuqSV-}snuOw=^RRkt2};_s;Tj?if%T*GGd zlCs9%$}w-&2c<>%`@@lvD}MzhV$EiH2mx$0s&&J5f|I9URvS|qHEC=-0ZK{~r!Rd} z8H>~nU2F{+deoj3cz8Ew2^uY(ra+oz9 zRVqH}E}p$=?-r&g3L0J~mas5??;Ax37z&{sy$lth1rqX}Cn1yQUWpZKuB~yBzN=;O z(W;sdW^VOk==uaKgb}imh1ra5{rzQR=PgR94qGxMU$+0aBshera3}~MojN^)M5(pA<|7bAe z-wi`lEx9DL28>g>&P%T@<2DGF+*pb+m!B7~8D>^Bso5p56qlfmuzr zvg^~yW{gY}s@z{DNWHk@1D5L>+2S8x#GTyO()k4MKLXu^HPSw{by2<})}Ldp?Oi=p zIi}#BrpcHq%J-B=0A``lNIO9iW_D6PhHtN1`v2Zz@JKK24LimoQEZ0#4d zYeKQ*#?NxyAU~OjoRP||JL7N-3;vk=Ux^^#oyeR>5yV88q#s7OQ`b{Y9qxm{uVLX66{Z0&@C0+_;{@RDVZe26QlzxhaxtJcW8ymX9D|o9pXtn}c*d z<0g-dt8sPk`9iS=3sRCWxWQcpQ-{J88pn+kN(h!+C?LoCW?eyj0 zFOY6<+5Z|KuKAnq;8XzdIrwZ=p|4Na?sxbmcw~r3NZ1qn9`CN&+%6~VL_c}H>Mh1H zb-5&;^+deRahTiI=xl0kdK@8X#0>bcH(9PqQh))Ch^cUfL9AC?lAuYv{>bi3g^ka8wggl7;0@+J_V=&htl}_PgE| zmjk&1K3jTLmge5ueWfgWR^#V5C=s%<0UgoE&0JaXOt-^4#<_FUpMqgLHIlN#3!3M6 zy1xlb7pBe4;4@nz9&DQ82}z>=G`m?-b5Z-skcVTqo+pUj8JbRh%IwROGPDwqneaGC z>K4#5cnCli=~GO#Cp~z39zNAzefmPP=E_z93vRc$9W+#_Od=F{rtRN-7Bn}zu3QRb z>g(ziORYp4l>xv#Zvv%a0fWPf!ByD-s!ez9Du|152$g(9^7$ku{0pE@+-8iEUm{ ztrPmiL?RI&A6X^~egOsM{@%h)c$Q8#)e-;<6`e4HhgL*1x-dG>bhUHo8K@_DLvf6% zY00PLXJVGktWN(BB62>srHvxkWLcA;7Q=i%UZ>>3D-0q!1bY@s+=_o^HF!L=n&GS9 z74(GoDL2<+IdAu`MGJlF^)Z`tql>!#i4F`}iMb_q?W+BFBf_5#;*(Sm6bhlGeo2LF zllb5zDmO-hRwwVY(i{7PSj#2VuajHbcqNkTu)>EUc7e?b^vhtsi{7`e@0*Ntpun6U z;B`!7l;u=cL79YvtC3_eVzRX^tyk5VSJenHt-++;E}s7^A$%uYT_iItob(+X!lVN# zY8F+BB%#lt>-m`(pcxsx=W55j;ophSL#KE{D8~65EmfjY0!%DnI^-h*2@Z#@zcoI&C6v~`Fjjk zbXfYkPX7HaX`VM;2TOhL;~Apc-3u%W;!pS? zMTRK0tBO_;W%~?oH=7{L@OkXJ(IEBCJCm^0F|ZiXJ5gxSRAG7GVZJ-=47y`lzc)R7 zw}8?_^02r!)y#bJ`F?R=_@(ebRhHkXiy{6TPG2bMPi;U!-IcplYr18t)$puHe2+VtaBn zhrWNqu=Smod6UKaXG_J<&UCI%tzx#dck?3~^ZR=Q99C2=tYDxU_63t)1yTF6k#ZQu zHX^?lOX)s&LjY)#XOM@|Tt3XxxS`~4gr2rHZ}AV3e@grx9RI)cd`+GkcMB;&nRmty P+=Qx Date: Tue, 3 Jun 2025 14:00:46 +0100 Subject: [PATCH 08/27] lint --- playwright/e2e/modules/custom-component.spec.ts | 16 +++++++++++----- .../sample-files/custom-component-module.js | 3 +-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/playwright/e2e/modules/custom-component.spec.ts b/playwright/e2e/modules/custom-component.spec.ts index 0a48c800c7c..7d918162314 100644 --- a/playwright/e2e/modules/custom-component.spec.ts +++ b/playwright/e2e/modules/custom-component.spec.ts @@ -28,11 +28,17 @@ test.describe("Custom Component Module", () => { await app.client.sendMessage(room.roomId, "Simple message"); await expect(await page.getByText("Simple message")).toMatchScreenshot("custom-component-tile.png"); }); - test("should fall through if one module does not render a component", { tag: "@screenshot" }, async ({ page, room, app }) => { - await app.viewRoomById(room.roomId); - await app.client.sendMessage(room.roomId, "Fall through here"); - await expect(await page.getByText("Fall through here")).toMatchScreenshot("custom-component-tile-fall-through.png"); - }); + test( + "should fall through if one module does not render a component", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Fall through here"); + await expect(await page.getByText("Fall through here")).toMatchScreenshot( + "custom-component-tile-fall-through.png", + ); + }, + ); test( "should render the original content of a textual event conditionally", { tag: "@screenshot" }, diff --git a/playwright/sample-files/custom-component-module.js b/playwright/sample-files/custom-component-module.js index 62cfe883daf..0b547a69ab2 100644 --- a/playwright/sample-files/custom-component-module.js +++ b/playwright/sample-files/custom-component-module.js @@ -13,8 +13,7 @@ export default class CustomComponentModule { const body = props.mxEvent.getContent().body; if (body === "Do not replace me") { return originalComponent(); - } - else if (body === "Fall through here"){ + } else if (body === "Fall through here") { return null; } return `Custom text for ${body}`; From f740dc3829e2758e20ea71e319503fbd7cb3bd68 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 5 Jun 2025 09:57:50 +0100 Subject: [PATCH 09/27] Allow passing in props to original component --- src/events/EventTileFactory.tsx | 17 ++++++----------- src/modules/customComponentApi.tsx | 3 ++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 48d2090c4d7..99c77fa172c 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -264,7 +264,6 @@ export function renderTile( mxEvent: props.mxEvent, forExport: props.forExport, highlights: props.highlights, - showUrlPreview: props.showUrlPreview, }, undefined, ); @@ -300,14 +299,13 @@ export function renderTile( mxEvent: props.mxEvent, forExport: props.forExport, highlights: props.highlights, - showUrlPreview: props.showUrlPreview, }, - () => + (origProps) => factory(props.ref, { mxEvent, highlights, highlightLink, - showUrlPreview, + showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview, editState, replacingEventId, getRelationsForEvent, @@ -322,9 +320,8 @@ export function renderTile( mxEvent: props.mxEvent, forExport: props.forExport, highlights: props.highlights, - showUrlPreview: props.showUrlPreview, }, - () => + (origProps) => factory(ref, { // NEARLY ALL THE OPTIONS! mxEvent, @@ -333,7 +330,7 @@ export function renderTile( editState, highlights, highlightLink, - showUrlPreview, + showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview, permalinkCreator, callEventGrouper, getRelationsForEvent, @@ -366,7 +363,6 @@ export function renderReplyTile( mxEvent: props.mxEvent, forExport: props.forExport, highlights: props.highlights, - showUrlPreview: props.showUrlPreview, }, undefined, ); @@ -392,14 +388,13 @@ export function renderReplyTile( mxEvent: props.mxEvent, forExport: props.forExport, highlights: props.highlights, - showUrlPreview: props.showUrlPreview, }, - () => + (origProps) => factory(ref, { mxEvent, highlights, highlightLink, - showUrlPreview, + showUrlPreview: origProps?.showUrlPreview ?? showUrlPreview, overrideBodyTypes, overrideEventTypes, replacingEventId, diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index 383a97bf6a1..fabb7fc6f59 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -9,6 +9,7 @@ import type { CustomComponentsApi as ICustomComponentsApi, CustomMessageRenderFunction, CustomMessageComponentProps, + OriginalComponentProps, } from "@element-hq/element-web-module-api"; import type React from "react"; @@ -30,7 +31,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { */ public renderMessage( props: CustomMessageComponentProps, - originalComponent?: () => React.JSX.Element, + originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element, ): React.JSX.Element | null { for (const renderer of this.registeredMessageRenderers.filter((e) => props.mxEvent.getType().match(e.eventType), From 9136d841ee3efc06f3e74dc62e7b4920a29fc584 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 10 Jun 2025 11:28:15 +0100 Subject: [PATCH 10/27] Add hinting --- src/modules/customComponentApi.tsx | 48 ++++++++++++++++++++++-------- src/utils/EventUtils.ts | 5 ++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index fabb7fc6f59..487ab1c8985 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -10,17 +10,27 @@ import type { CustomMessageRenderFunction, CustomMessageComponentProps, OriginalComponentProps, + CustomMessageRenderHints, } from "@element-hq/element-web-module-api"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import type React from "react"; +type EventRenderer = { + eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean); + renderer: CustomMessageRenderFunction; + hints: CustomMessageRenderHints, +} + export class CustomComponentsApi implements ICustomComponentsApi { - private readonly registeredMessageRenderers: { - eventType: string | RegExp; - renderer: CustomMessageRenderFunction; - }[] = []; + private readonly registeredMessageRenderers: EventRenderer[] = []; - public registerMessageRenderer(eventType: string | RegExp, renderer: CustomMessageRenderFunction): void { - this.registeredMessageRenderers.push({ eventType, renderer }); + + public registerMessageRenderer(eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints: CustomMessageRenderHints = {}): void { + this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints }); + } + + private selectRenderer(mxEvent: MatrixEvent): EventRenderer|undefined { + return this.registeredMessageRenderers.find((rdr) => typeof rdr.eventTypeOrFilter === "string" ? mxEvent.getType().match(rdr.eventTypeOrFilter) : rdr.eventTypeOrFilter(mxEvent)); } /** @@ -33,14 +43,26 @@ export class CustomComponentsApi implements ICustomComponentsApi { props: CustomMessageComponentProps, originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element, ): React.JSX.Element | null { - for (const renderer of this.registeredMessageRenderers.filter((e) => - props.mxEvent.getType().match(e.eventType), - ) ?? []) { - const component = renderer.renderer(props, originalComponent); - if (component) { - return component; - } + const renderer = this.selectRenderer(props.mxEvent); + if (renderer) { + return renderer.renderer(props, originalComponent); } return originalComponent?.() || null; } + + /** + * Get hints about a message before rendering it. + * @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 exists, or originalComponent returns a value. Otherwise null. + */ + public getHintsForMessage( + mxEvent: MatrixEvent, + ): CustomMessageRenderHints { + const renderer = this.selectRenderer(mxEvent); + if (renderer) { + return renderer.hints; + } + return {}; + } } diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 9c387b16b04..73d4ffebec6 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -30,6 +30,7 @@ import { type TimelineRenderingType } from "../contexts/RoomContext"; import { launchPollEditor } from "../components/views/messages/MPollBody"; import { Action } from "../dispatcher/actions"; import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; +import ModuleApi from "../modules/Api"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -77,6 +78,10 @@ export function canEditContent(matrixClient: MatrixClient, mxEvent: MatrixEvent) return false; } + if (ModuleApi.customComponents.getHintsForMessage(mxEvent).allowEditingEvent === false) { + return false; + } + const { msgtype, body } = mxEvent.getOriginalContent(); return ( M_POLL_START.matches(mxEvent.getType()) || From ec13bdc910c5a000e8816390ad751909fbba657d Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 12 Jun 2025 16:25:23 +0100 Subject: [PATCH 11/27] Update tests to be complete --- .../e2e/modules/custom-component.spec.ts | 135 ++++++++++++++---- .../sample-files/custom-component-module.js | 39 ++++- ...om-component-crash-handle-filter-linux.png | Bin 0 -> 5872 bytes ...-component-crash-handle-renderer-linux.png | Bin 0 -> 5730 bytes ...stom-component-tile-fall-through-linux.png | Bin 3803 -> 5930 bytes .../custom-component-tile-linux.png | Bin 4098 -> 6174 bytes .../custom-component-tile-original-linux.png | Bin 2056 -> 4717 bytes src/modules/customComponentApi.tsx | 41 ++++-- 8 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png create mode 100644 playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png diff --git a/playwright/e2e/modules/custom-component.spec.ts b/playwright/e2e/modules/custom-component.spec.ts index 7d918162314..8299c0de118 100644 --- a/playwright/e2e/modules/custom-component.spec.ts +++ b/playwright/e2e/modules/custom-component.spec.ts @@ -6,7 +6,8 @@ Please see LICENSE files in the repository root for full details. */ import { test, expect } from "../../element-web-test"; -test.describe("Custom Component Module", () => { + +test.describe("Custom Component API", () => { test.use({ displayName: "Manny", config: { @@ -23,31 +24,111 @@ test.describe("Custom Component Module", () => { await use({ roomId }); }, }); - test("should replace the render method of a textual event", { tag: "@screenshot" }, async ({ page, room, app }) => { - await app.viewRoomById(room.roomId); - await app.client.sendMessage(room.roomId, "Simple message"); - await expect(await page.getByText("Simple message")).toMatchScreenshot("custom-component-tile.png"); - }); - test( - "should fall through if one module does not render a component", - { tag: "@screenshot" }, - async ({ page, room, app }) => { - await app.viewRoomById(room.roomId); - await app.client.sendMessage(room.roomId, "Fall through here"); - await expect(await page.getByText("Fall through here")).toMatchScreenshot( - "custom-component-tile-fall-through.png", - ); - }, - ); - test( - "should render the original content of a textual event conditionally", - { tag: "@screenshot" }, - async ({ page, room, app }) => { + test.describe("basic functionality", () => { + test( + "should replace the render method of a textual event", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Simple message"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot("custom-component-tile.png", { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, + }); + }, + ); + test( + "should fall through if one module does not render a component", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Fall through here"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-tile-fall-through.png", + { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, + }, + ); + }, + ); + test( + "should render the original content of a textual event conditionally", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Do not replace me"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-tile-original.png", + { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, + }, + ); + }, + ); + test("should disallow editing when the allowEditingEvent hint is set to false", async ({ page, room, app }) => { await app.viewRoomById(room.roomId); - await app.client.sendMessage(room.roomId, "Do not replace me"); - await expect(await page.getByText("Do not replace me")).toMatchScreenshot( - "custom-component-tile-original.png", - ); - }, - ); + await app.client.sendMessage(room.roomId, "Do not show edits"); + await page.getByText("Do not show edits").hover(); + await expect( + await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }), + ).not.toBeVisible(); + }); + test( + "should render the next registered component if the filter function throws", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Crash the filter!"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-crash-handle-filter.png", + { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, + }, + ); + }, + ); + test( + "should render original component if the render function throws", + { tag: "@screenshot" }, + async ({ page, room, app }) => { + await app.viewRoomById(room.roomId); + await app.client.sendMessage(room.roomId, "Crash the renderer!"); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-crash-handle-renderer.png", + { + // Exclude timestamp and read marker from snapshot + mask: [page.locator(".mx_MessageTimestamp")], + css: ` + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, + }, + ); + }, + ); + }); }); diff --git a/playwright/sample-files/custom-component-module.js b/playwright/sample-files/custom-component-module.js index 0b547a69ab2..563b4123c5d 100644 --- a/playwright/sample-files/custom-component-module.js +++ b/playwright/sample-files/custom-component-module.js @@ -9,6 +9,38 @@ export default class CustomComponentModule { static moduleApiVersion = "^1.0.0"; constructor(api) { this.api = api; + this.api.customComponents.registerMessageRenderer( + (evt) => evt.getContent().body === "Do not show edits", + (_props, originalComponent) => { + return originalComponent(); + }, + { allowEditingEvent: false }, + ); + this.api.customComponents.registerMessageRenderer( + (evt) => evt.getContent().body === "Fall through here", + (props) => { + const body = props.mxEvent.getContent().body; + return `Fallthrough text for ${body}`; + }, + ); + this.api.customComponents.registerMessageRenderer( + (evt) => { + if (evt.getContent().body === "Crash the filter!") { + throw new Error("Fail test!"); + } + return false; + }, + () => { + return `Should not render!`; + }, + ); + this.api.customComponents.registerMessageRenderer( + (evt) => evt.getContent().body === "Crash the renderer!", + () => { + throw new Error("Fail test!"); + }, + ); + // Order is specific here to avoid this overriding the other renderers this.api.customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => { const body = props.mxEvent.getContent().body; if (body === "Do not replace me") { @@ -18,13 +50,6 @@ export default class CustomComponentModule { } return `Custom text for ${body}`; }); - this.api.customComponents.registerMessageRenderer(/m\.room\.message/, (props) => { - const body = props.mxEvent.getContent().body; - if (body !== "Fall through here") { - return null; - } - return `Fallthrough text for ${body}`; - }); } async load() {} } diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-filter-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..b144ca6a5ecd5a82b5437f939b9bdd40e257f43e GIT binary patch literal 5872 zcmbt&XIN8B&?tzAiXgm55fvmfrS~3+gf6{FM~b1NfV705s1Oi{bdcVA=v~N5haf~i zN(jA$-g~{_eeV5ppYPvyp7Wg9v$IonXLe?1Lv^%NDDE@fCn6%EP*Vlz5fPDy656VF zZxO!o6R#x+fY?({U zeenLEI#plieP0DT^1oKp<}d%6e!!_02j$WxFxAxBl!m2c4c!`|`xR6g)Fk@H@Q=lp z+E1ly$RD%nK?&hOr&(Z`+_Uj&3P=XUdwP|W$jYk!6aEw;S_tZp8y+KD?*N1 zwkU%pTQBR4F4H~B|7pNV^|+o>K$oe#$O#g64%Xj_Fq)z2=GOW*L{;`{FFl&3Cv}04 z!}r(4RhTA^NcQ2!nm3d1VlC|lE3?ufJP*xaV!8aE8JGgU?^#C5vSsM{OdE z1iXBoX-nY#%4l2US7CtEp5feJ>9=Qv1O(Bafa)KZf#uYhDxtcC_6?JyVi~>ZU8x(C zHqi5QBRgYbs(I83-sHEbUrB(NVwu^&E^3y9pjh`KQj6KM=4UzBmRC!UOt(e{q! zl@yOJR|p)wHqae-*@>rjpTzd0E+50fhBJccY4clB;sZ=QX9gvoAe#8S{Ol08f_|X6 z;i`@E)J|A)bQ6lP@Bx8qngj08(j_LlEF`Ycn3kg;Do_e;lH2~tr* z>@`l7W%sH(F7vZzu)IGw57X1=YOS2TX_Bu^)R|3&PB46fU~?uX=gEeEb6uTS*z%><=+u%ZH;zl2=@ z-*tW*M;Znh{1#TN7hkZ22pYO57MbW&kUgMomLr&Jh41yUaB3Iyhfs=~o?j|F7y)%O zM@@^db|dgqgG=ds-2Ij@GkVw~+maBBXGKP_;cDkdKEg1C8Ioq?iQ9Cua>Qd`5DQ^)kxvH`3R?2wMH8_d4-S$=zpB(Ugm61Ml|7X< z&&?)=4TseH>bTkH*73biqwdCk3uS9 zQBu&`Bk2SD{ZgK(Ja@=igzP?bPHld~A2BmrPqgNe6*;a@VN8a6Qh8s;*}QhGch}#J z8WOa;TpbIlgxX`rIuFB}H_ai!AvZyzI;?$^9Tc$f%#MbPN_R;i_w+jk4wqgW zToMLotji;aLE<0n=0u%i+XG@u!Vjw+=(>7`+4Ri~@rE{Nkh^>P{(=n*t3>3jH+&Oy zxn|j4B$0fEynC~FLh0D>3b)m9|0zs0uKHFcxvT5He2+V!a z2LeA&V46$X_Es0{HK*>|ut52RF_MEP=YHeKWfae|%s63ZkXekA_YcTKTSJV$+`%~s z++nazVZP8CvC=PdXCsbRV<;=^obSyxsyBCLVe2Nxq}?F|8fBDBm@RgGWNQp|eq_#0 ztvj_nlVG@JHl92@V{@>mOHiDdy_-dj_C8#nxz(&Zybz}*j!IHkP~a`V9M*G?m~@g zVE*a0Bn354{O5Jb!jC8|#cp32cggx~RtMS}*=7!4S&|z0}a8HZ8 zl|QR`_95yUz-TG&0b-)rd!5t2l8!Dvmr4u69rikQzter?F{Rq8{C;q>i8Ip^5;|U< zZZsSl{i&@aD;%4za_Y<5|Mx!7M$GLE{pxRhurlU>>-LO>Q5-c}(usHX(5IIZin;^Q zWBP`h;nK_g!Y}k~$LIypc5!-QDxvk8Zo1jP1 zxY1iEY{ZInO9*dCOKM@Ts;M0SPT66X^!}Cwb6Z@@2!joSO`_4pJ;HLvnboa%Z(I$u zwb|(U?y!MFH}5LUfF%Ob=f)$e{jm1t25)pMECSR`M>lbGID?~?Z)WV`nr6QD;k$Ek zmDQfS3dKyGt{v1t=Z#$BbANFz0k`g4^OUrhWA3)MUo>w@Q9TlAxTm3CvqvH69N;1W z0tsKd%6_yLDnId+2PpV>0PQNOKmEv3`2@aQHzh+4wCh{^ne~3ps<=aU++f%HQ zZGU?UtaGsnD7o2JiE)3OmG7nsMbg?TZDo}v69mLxcT_g8$t(@jo0_yhDgeYqED zh0oa7gSG5r0F+beUNE^5O#mQ4i$bVa44H4MRZmTVAL6m zuD5%>Ip|>AJ9(1hz#Q7F!xU4-YNQ{__+)FrhX2t6`o>>Ofh$|+$~8xm8=r-=}6SG^R-4ta)(ndmoQ~kE6S{N3}|}MRLDQk6PlsBvq{rBhzxE2$o1-q_C?tYB0$sK!>&bCC6U9t8D0B*Y1T6~;V z_(HnHGb+SuPdY?IIXe?@Dawh>`eH~ywxWehbK{0B_2*27J(zfs;;Z@7W)fjXwhDOA#_wPKj?D$a=z=KSG@A=y$?| z6e+Y`)Oox0fPdy;qBTwexjb9hTU)6_#{IxhMb3*>+gDiQW4i-1qwf6g)9d77FK_k2 zRw%CPXO{Lamk>2@pogl7Zf;SLqp6~g&=IXl8A5ARCI_Z?trGv5;hpeXn4YC$Twa#C zPqr7N{BNtM$9d~Q*n-M2^dEsyL2Jk=MfPBUG?O&eND5-_uj^O{aK*ZCV%w_kD=w^v z+2VQ5g?OEhyLwh5#V9>%f$y`N9&t}~O#?sEuDVN1j88Q3UfAK&m}>(Vc&eUpn0e)( z9Yh$5DKGo6ZY7v#{$a{ZP2ADj^53m5U3^wa@RwIH*^-rZ9_{(>vk#^N5l_G^2T@v< zet1+^Avz+xX=Y|ftEi~RKvX>3FoaI>JJ=m)EAVlY5WEY$X1i@UwNeFZ4imDjT~4K% z=kUJ*ul62x_Kq6E=EuY-O495f%c7pe(3e=6_3x!Bwn00LKrHF+dTKiGG!@62xi80) zisSkE%;^D(RMdW|4VlEf(vRN$BMz|PnR@jK3?~I$tds{xI=^`STg64uu1bOn|2}*^ z*1M%(6=YC#2YsB!MM@r1C&t(2-*orj^X}tQr){Fo$I(l~q}!!tC`F?d#NmZRm%er7 zZ^a}xTrvxQJQI)8wag+Lg_%6-`==fM6PRiq73ftyiJY@8{^P0wg?GClJ3VtFikp81X9|42L-eOVHnl8qLue~A8TQ*aX`d=_ZZJ--?U_ zdu>#F@=NC+&vVDE*h(z|Q<7?%zkxd5yD~94K4wZ_F)9X!zK-k>fVKt@M;hYUwsnfC z*P`|PNb!f}#x|kwZ8|zGOR8;g-Vv>p7g4><0l=+`GP~P0X0Ud|)8Ia^YF~8gZ#W3l zL4TJtLSy`a@xS(wt0sVehOC@MHlG$>$gl-jf+IGvi(NSGR8!8!sb2aM!{jqU3B38& zvU&74sS9Ok?Bqp4wgWF{X}Jm^QGxMFUw1iIZONK1U_Z;a9$uSTorYq-9X_%5$5Z|C zMTl<|bD&1!Hv4+O_MR8t-bL0#m!Y}B^_KB=&??X4FbgZU9b96=0OFd{OVfu>6W_!W zZZ&>41B}eg%C(`g@f0be){$3QBLC?t!6TXvHn7=9Yd*!Wl5Ysp;Cz@ z1fTWsQt5E2SL};X?%UFiw6gBa9$`Hr1abAV8R(8B0bnzHE@ExqXSqEp%fUMC-%Q8ZR) z3!7tDsMH8S{iSt}ytJ-*bv^qap+yFtXj<<(+@&pdcmFtEZ6gt)AABdmw9}*0O@Wzh zQ~x4mMY&V5b4?4TOlJIoPJqYxWxQybV(1S+0jNT^`-H2$eVPsztx3&?NE*VS-iyS~ zJhi2E4lfBy0|#=E?-fsKrE=T9?nkYGGnzYvDDzK2t_Va;zb=$S_0e@3hyM1#K+;1pwReo@U8!8IZIIBV3qbLnlu} z*V0{*cv6$4GI*GVVXUK}Qj*|)P@D1Qi_s+6j*Sc%BP&bfXhC&wt%1=J&%;FO3jI+0 z@GT-Q{x;qAKyK8ZovK;M^~b}Lp$_5MmtV$`Qn`WdVCk&Gxbz^XatHN*=0rmCXnL7! z%FgKgilKB^kFCW-67j5ry9P_u*JOgMY=W4YBm4>=?j~evT~ayYpqmE zBQB2Ig)WS#H15Dl%;}UH9BAI5o2Ms?-&uFCJc(u{4KsAVb9>;Sm7znOou?kWpxKk% zTy5M&?u^4aTE@jEv{jv{PIOQ+{jsP%-^YiR#(ZJZjVz$o=qc_|TJkL0FC8-T@GtWK zCG;V;H|=VnpZmKzPJuY1O4`R{bt)R;S&Ou~?xm_dBVnxK;F80@anbh#2a;_~=rxg2 z5$hG_L#)#S+|8F3?vjkU!T(9I5Xb0Cy_E3hU$1ph3^yw_7MFa5QVn$5vZ+&3SP$!8 zVoEz`&WXr;y%k2}Ly!Hvq;!#y6S3VpAsX0Ku@ApbM;9>5o!S6U8977q6gR&y(wlyU zs<=g{+G1K>oaL^}93Dns(gG9?pdC*>4+EF_y_-I+kA}_*578@?mQA*+y1tnX5`v>{ zvD801aKzCF|G?bHl`2v;hAeNm+oOyP9QlqnMuvaN^;k;iZCHb`C@bkAmj8RpTmPRL p8P2N_Cfo$FPkf~N|MR`?g`7Is{6E#9w!jy}C3JD1ln<`jA&o6yr+R60ua1+ILbB-*1l0w*(p(}5%C_rd# zNhEz%(0i)Xb8DK#-|E93#9-_50^AT&E<1%NgdSdi*&zrK$e->Atqpke+JmuDm&}D~*K9}~8l!%<6GcOY)vZQ!w zqIlYHKkEunI^4mEUKVe>3nV9sFJ_<6m)R<+T^6Evjx9rl;S6J-DY$B=z% zjh5+xA0PfmJXYIeP_cy`cvsVaTg3-(sjShS!ruEe?zhI5V}tFk+ffnqZDa3_%*>!* zZc0I(gEb#kPD7lXpyH0-`-HO$rbuTP+^JK3C36uQ{?rBk4IG)#O0h6K4U0uUl zESCsd#|2xo;GnhKCP5KvFIi}a^-tvv&~D~azOeh4XgDx15I^74ef1l$f@FLDMP;xL zXCt;HU-@MB`rG(!>fF&LKhGz^QAB%zASzv%z$o;3{sFYuz5M!C$l=JlE%U|(@+XIEO=(mZv#+;CLq zHep=(p1zp3xCf4`DRItbYDay%I;2;<>szpmSJ%wN@sCw`n|7C>`~oPne$p%0WPN(* zulV{cv&)r|`M9vDv41Gf@bFBQeqn%>wwxI>Vs&Qts@DzoxD_9k|Mt96jP2__{>>or zHmrJ8CXLfMvsMD$NgI>)&FJ=71O1~Rpdj%E@9*Pe^Eh_^8>??0H`#OH>sjuf3d@B^i{!lHM zpnnp<;>!qbogr`u2Iuj)pRS~LNXX+pSkW+Xp`;%{$6M{zwU}4v*6JJP&F$S`^CqVd z|5l2OCK;}f`$S96SW?r{tZ%ZJH9j7jhzEU=exlEm-F;{4ToQ9mICYc}=(XoAx>Ii> zUQG#jOb1*7DT4E?_tRhJu+Tl^?>5$p*43@K*w6G95y@GK_GGMue?=`B+R?;iPuet6 z$ZeWktmGf0+z6A-!T%n`#iHszH(>6q+^DU$n>5|q+$`=En{astJT=k$y}3mX6<_(@ zTYgHVtz~Daz)lx8uQgs?Y{zO{H<*tuXC)e2rE6{dcQf{8Gya5M@oErFJ<_sOe5%Uz zpzk|+2$e&0=x&iebiqbkqLd%rF{SL_qSnf+5jfKYhj+Oq>La`C9lL1XBsOHE0im{w z_EU~^2FY2*DmL=+{hLP-q>ypV>7C{#i#GWSyvdBr?C!XTMQo~*4&~j332(y8^%KjZ zGx}2m`_5qgNl~6DRFh3+()Jm9y&1x=<978_mtfCV-T+^Ufw2cCr$lS#VwpntcDAxZ z*r!r4+1>Luxr;V)#j-k!pcD_4I>WjkfQqVAdCic}utcOl1l%!%52crKeK>Ig)Hdaj z-vAsoVDCnC*6AS>_mk>q(X8@QinCp&xWkobGRsKRhgkCTS6q%zG0<3U+t~A$a5%FZ z@`gXSL}Q-6dzUjVa%AShzwS<&4yU1|X8>POC4Ihs&Q-FVu<`9KP~zsX|A{MA@y0iigI&3ipU(LXULcEkVs9i9 zTsZ%x>c!(=w)|`<-2@S)g|_o}-^x+_e0+~p#e}ZAi7g0=)u(fZq@)p-SZT&LXB~;Q zsSz;Y^Jy&1C+2+;aDcfxA2{WznIk*^nx+;I!a>d!&LXPq?&5C#)-Tugp?&KE2HR~W z5ZdO4q5nLLl&aHZqrhmTIqHVdmo8xsC-TH}BztQwfS97y`bUfL0(UkwL);nYrkgVZ z7Nynm>|i~w&R^xJyQfr&EAILFZZ;c}F~`M$H)c zqKr-Jh_I$Mie9%j^rRl%^l|%+R|@8)(^g;U%u%aLongCbcA7Gh+Gji|Zj*in+-=<3 zva4m4q8gRHx!b}{)Vf{B$5(IbHW}e-OB?#W#9Kl86&ej~yhp$`;B@6rEN}5*j!ry3 zT!@&wkBNOh5if2zyNToywYOZC5oN+G@eogRddxb0%cb}@$Ug(t%*c>9!;jqFOBvYk z%wMwyi@6b~uUgA7vY)j6G;}k^_WUY3#EI5K%CNXwusw*}gGdsw#LsH#ThWs6>dl_6 z^Tjrb08nqQ(E7L+0fISODIl~(I1wr?2Qo-RONMJWOwC)i3^5tFjR_kwUXZpw zU5T^4bkW3@rtE>I7K9T$b#*7*gn1;^Q^FM?dj>OC*y;R>W1y_buZH56waCf+y=iFv zX@jdQa~kf0dcd?rI(;2oF~>}|2Km-K*QJ3%?-4p>8Sn6s6B#AskTW? z%gN2{D0x$C!zgNEN}DR zkL}0^M9kx@7OLpzN73eTRScL*SmfYj$7}4Ji%u8z9vnKqdgbEelqL~SutY?q)wR1c z2LYY-L4Mo)u(vIGvyq@+{9afP2yvnFEvN7ggXb3XuiW{nn5n%m2k_F{$GOqsqR=wu ztZ+XK-+5Nns_JS@_w=#Cx0;kb#Vn;bTu~oLHyc&3<3G`BGP-$|YC&)c7N`4IiTjvl zW49>gvpW?T&hy@fpMt3D=e>ugNp~0HW!~I{-hU1mPfS$2YYisMS8i| zI~lU(UaZk||8SQoz+}yHetUn;!TGq}B_9r8|sefbF zqF3W%fE<;tBn_tv{wA@-Q>yRMH%iTfm{*Z!=_YflV)>&y#s4T;)~cw#>BoFb)lzMC z130m&Ca1!Gmuu48B<>$g*QasrN+vzvs6Qc64iT`rjQ>~8S2j2rQh?RRzhDpoJ`#N_ zR#si3H*)14Wq-Nz_FP`oUZ@M}{odqoQ*L~vO8rXk4F8F&2wo#KttzM&Af&8*C8T2X zciFP8((W&QvtZewanUnpZ;Q5$irFJyW9Fnn=3-Uq@YT<+k6vZBhhr^B!CZ~ZD?G^u z1%^++2Sk^gJ79!_K7f)Xzl&dZZ-3ZMO8U0B2gZsj?UchwnrV3R-qocYZI0Kq^{RLP zcJcjV#R?xBSYLL>3i%$IPS!FXfi2;daDSKzGx6jV`GXOTb2@jL%nW*85#@7~DQI`Z zd)LAuA1R~bjSkLyj?^5RLQ6nwx1aEGe0+_$O{e{Huvs;xk?B^o_zb6I?gvPzM3}-) zd?hc@-@?m~7&d$xQefdGH*o)m$?)N{NP)ig%7*BxSry9cJxBj z2)S>#C;%|OO?c9HhlTGYoS}~5eIcwQ@&j#(cq9mh#{dvEf9w}V$k872CX~ksS4Zx3 z6mR~Z>15(oia2iYp5mg3kxf0F(`OgZoMl{&x$-uQuv=(pj{u+iiLFO< zRZVFNvdBpuzdj}e3v;r#q21syg4sda??zcYf!ys&5Uz=Cr{N@W!M<;DICYTc*as!kGBEOa>cavq8&)Vg!8}{|iG|2-i zIkU?Lo!UiNwhp%bxWcsgni(n|oXm3`iNMZfgd%;v?;KMdT0HEFp%LZ=I0DakjQ9;X zswMm@;Ra{;$utmJ_f{NWtHUW<`x~}=={-T_32-rK-ISe`b8}H&P@q#I&d)`&hhd9` z@l%v`G1tP&_&`^^XU1y3A=%jFrIOA?Nti8-S`Z^r@yl z1jL*t#a-v>Gfw5<4DNp>pK;1>OguX382wYUjPIb90<+(6>A$!`+_4?r5*e(ZK|?Ao z9g!X%Y_f~wQFKkZ@L!#z;zg(9SYk0Ou>)tuu>^u9nSi}fc)M;2C9~odbPZ9zEfbu^ zn2=r_De~9~4ri+~Qq5kmiu|3X?4c^Y)2N6*-}X>t=2&C9Z|?^IdjP^y7|N{!LaCDY zIws>t4J??{v6UKiML@OsA7NV?(KpKO11y9xW$!}*73;g=(t`GrTc!9P1cyCR1Y_6Q z63PE?prg09H<(0NP@ZEa_ivhn!)@l})F4vybfY4C^W1SIba5-Y9iOmsoGD7qpTf=t zF?!#+3+P22kkZoOVLOXgEWd%ldKoam#Dht9G2o}?U@VS1Z}EXD@2_I+G-1ajae4fy+JM)~y`|i!>cSFUinHGA@3Mi%6%oKxyi1z?M5z_6elfw(X zu`Ew2O7*TDP0gtSO*1hxPAgGt@%NGFl$qF%e(Nl>Bz01Ena4w95q- z>!0Dl_~IV*@!Pj0>FM0rl9b_tPf9Zde7vf>z4i_>*KpH}l74|#vL>O?Xj@<>)VwU0 z)54)8Kvs|UuprdM%90*Km-$;HFrQHXnZdvdp(+kIV zMA{l5Y~t4eh^>u|wW2P1f0@0}*uiTR1h%KF$8Y>fO}MPMZh6-s&S3 zWWu9aT!sp3>0}S_(S8Z0$LpAVr{P~`5u;f)N}cVZhOyiV?_3Ftum}sPk6lZrkG%G_ zgU3O_b3-+?mC~t_XG$`Ee{@&=)s5uQnBgJm&xzNX6N4{N^-cyp!rU_9!?o{a!3w1>qhQbW0pNyeK&O z8#cg?Q$o(rDU+3SsR$qSyF6~2q2JMkbdmz?O(y`u^7&zKK zzU$JO*tzD%V-0su{X)F!g=C~zsDR}~r`b2|8QrfJNQh;bjv$L}zrd4Ng$lXE8mY8P zTq}XY<zeqc5GFqH5ZIW*mJkL67J`Jugg{Cnc;XW z^ioOBL%)toVsRNmePicZkwlH0EKPVfH%zr0tYcoXIwh`qVX?z+3 zd-<+g8m-NtY=h=rCfLK>boqDLv0(ah;#i+kyscjBou6a}?X{-}rQ#I93B2P6Q}zf8 zLn|jyg}tuWixU<)_iO6b2`&k=o1jl@dlL_&Rw%*UzC0)su637-rf&{=r=_kl{;$K( z?X4LM6%WPAB4bjc8sbykD>J)H6vBkPEfLgEGcnsRkS)T0!6MN>^3c0f>x1 zlByydkiDRpL5vEfe75>hiUSeB)HdtQY1(CNNDQpfUM;xfdUkO89Tk)b^Tw$)zz> hlq3Ie@c#r?FK(vM#UR+~hZ`=NsVZrKQO{n!`wzxMALal6 literal 0 HcmV?d00001 diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-fall-through-linux.png index 952b0110b6526bed692a63031cc063ba9fd7165c..0fe98072a04c150c34261f4201449f319db9cc94 100644 GIT binary patch literal 5930 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 Q{(0l3EUy8qkTVbdAEtwtegFUf literal 3803 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 Ruqyxn002ovPDHLkV1k8JUu6IQ diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png index 410365982b864a0a97d16e2a78a4acf3421ef15a..7c5d6b66e6d2d753dbeee10cceba0445dd1e6782 100644 GIT binary patch literal 6174 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 AG5`Po diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-original-linux.png index 45c227958337a4e3020726b071e05517771d943f..9a00a3b04baee2147b7447734180bcee6bf5683e 100644 GIT binary patch literal 4717 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 KD|+(s?f(EcI1Dxb literal 2056 zcmV+j2>17iP)Px+!bwCyRCt{2-EBx)SsDlMXWGCCBjmDy1PWg1OcJ&=nHE#mHVw{Nafx+zV%@E= zt&Q68ZPBq^Z4@1~O*chd>*`di)S9+V5|rAxb;?dUu|{2<|+^E`)q_;b#^khh*ce+~#Cgb)gwU2nPpgb+e#hr_Hz z2qA>R3$qp>gb)fZ%vyvHLMXg2YY{>Sq43%Te!lm^=*TEV@K1B(M1i&YYo5O8&i)5{ zT;m5ZC$7)$guNhy5ZbnO0RV^ybI{!OX;0r@I@&Iplzyiz(~!|PDfq)(o${ZZ)xKZ) zr4a5y2qA>f8xy=kCNf3rQJp^Ckan)D>CV8=C9SKgvT>Au)9oRI5JKDDmd|S?n$uN< z2Yus1&frzD!1daSPP`wj*ncEByR>!0z4=y_&f7B%8)`o5Z!AsM?N=xc8%yqT-r&iY zx<=Y9*#_-?g*q|4pw2NP0Qfy$T7IdkbP7KV7sM*GrNirv(ShsV>gJlvBZ_xpN`^e( z_qiT4TQUt=l|rpI7S#362x~^xIXWxTbsB}@u%Y0xV@BB2px-xlzrN&Tf;vhO85NUp zxqohPweuIdi%I^ls&!~S*mkIjYXz#?9IY0kc7L??WNEk4J=1T^`c+J1RJ86yQ8(uc zj$Go$Z&qb}a76KLwD$Pfn!C=;JtBk<+L1!M8x_IowJhZq1PZX^?k>-*o}fqb8`_%6 zQuZu#mgd>UJX_52xd;0nN1bjS`f9l8cVw!us+02pfG}n)D7CvIjTKEDSIt@&tIsO8 zyQnOs>2J;koqVtH*7O3`Vw8*4ujcnnk2)o1Zck1P*B+AzWA#~oaJX4x#V0Mb=d{Zs z*8CcWi>{8eH$Mbp(-)INSBx-b&9Cfttxqz3xH7+bh{l(-_4V|%@p( z)!lVy61e4YBX;lIv0sv#8f@YKtkGoAO765?60hHd9fDz+m?|8DwawJ0QlAP(ST)^6Q~xxTfe)b+V`{*UO55EkB6$L z-nea5QeJj~k`XC$^3orE?NFLbi9eSCm@b${$8U0f_nO%i=imrUX>hcqvH$?TPIzzk z*{lb{b6M&`lGpj#5JCuTBcZaE(nTLcNEk5{<~c#4j#aK^0+DOtlyLW3m)FF~H!Q|5 zQYQGnm@ov~OBB3Mr`aMjT zWV}Fu1puNtpkpI~pUU^?G-PCY-V5)n9ZcZaimzk^K+2xFSe2$+Gkv#AC4HU65JCuTFI#1;-#6o^@0^t!&(q08gs8Q0 zpL5#nOAOqyQep0q8Al zWi-Fg1s?0tXWSGoi+UU2p8!BzoYS?W)kFXQ2;XyaUPjN#39P=CMER*PjyZGtR;`W! z0KaFjAHW~T&h@+VduB!_ynB-q_lBco)>Rm8F{B3iFw1D^=9ldcH4{N z&Yw<>TAa9UvkvYy-@8+#5A47_V5=k9$!f93+53s($-d&IgKJh3N#s)Ugl;lL?*i)-UduRiX2ni7RVsB9>}v zMwbXWVaes4M?fqx%sfv{;GUvXWotI0^H z0hvC(_|$J6U;AOx?nDyR0d-1kb9ub%Wffls(U@CqK&7RzsP~TmM5aGe-*yp$79oUw z65iVOzZLJ}8Z&dhN$Tmlu<`w1gb+gL#{hqO03n1B3NOrBgb+d~yte-xGC~L;^y7x- mT7(cnD7-Lh5kd%|@cI`t2}ap;;TM1a0000 boolean); renderer: CustomMessageRenderFunction; - hints: CustomMessageRenderHints, -} + hints: CustomMessageRenderHints; +}; export class CustomComponentsApi implements ICustomComponentsApi { private readonly registeredMessageRenderers: EventRenderer[] = []; - - public registerMessageRenderer(eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints: CustomMessageRenderHints = {}): void { + public registerMessageRenderer( + eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), + renderer: CustomMessageRenderFunction, + hints: CustomMessageRenderHints = {}, + ): void { this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints }); } - private selectRenderer(mxEvent: MatrixEvent): EventRenderer|undefined { - return this.registeredMessageRenderers.find((rdr) => typeof rdr.eventTypeOrFilter === "string" ? mxEvent.getType().match(rdr.eventTypeOrFilter) : rdr.eventTypeOrFilter(mxEvent)); + private selectRenderer(mxEvent: MatrixEvent): EventRenderer | undefined { + return this.registeredMessageRenderers.find((rdr) => { + if (typeof rdr.eventTypeOrFilter === "string") { + return rdr.eventTypeOrFilter === mxEvent.getType(); + } else { + try { + return rdr.eventTypeOrFilter(mxEvent); + } catch (ex) { + logger.warn("Message renderer failed to process filter", ex); + return false; // Skip erroring renderers. + } + } + }); } /** @@ -45,7 +61,12 @@ export class CustomComponentsApi implements ICustomComponentsApi { ): React.JSX.Element | null { const renderer = this.selectRenderer(props.mxEvent); if (renderer) { - return renderer.renderer(props, originalComponent); + try { + return renderer.renderer(props, originalComponent); + } catch (ex) { + logger.warn("Message renderer failed to render", ex); + // Fall through to original component. If the module encounters an error we still want to display messages to the user! + } } return originalComponent?.() || null; } @@ -56,9 +77,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { * @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 exists, or originalComponent returns a value. Otherwise null. */ - public getHintsForMessage( - mxEvent: MatrixEvent, - ): CustomMessageRenderHints { + public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints { const renderer = this.selectRenderer(mxEvent); if (renderer) { return renderer.hints; From e60a68ea1a2427202f831fab0223ed4d1d63536e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 12 Jun 2025 16:27:58 +0100 Subject: [PATCH 12/27] lint a bit more --- src/modules/customComponentApi.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index 8e6eda22631..2487231c29d 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; import type { CustomComponentsApi as ICustomComponentsApi, @@ -14,7 +15,6 @@ import type { OriginalComponentProps, CustomMessageRenderHints, } from "@element-hq/element-web-module-api"; -import { logger } from "matrix-js-sdk/src/logger"; import type React from "react"; type EventRenderer = { From c6d4f38a04969232143c0c9efc3558e7141f607b Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 12 Jun 2025 16:39:11 +0100 Subject: [PATCH 13/27] update docstring --- src/modules/customComponentApi.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index 2487231c29d..71c0fc1a637 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -33,7 +33,11 @@ export class CustomComponentsApi implements ICustomComponentsApi { ): void { this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints }); } - + /** + * Select the correct renderer based on the event information. + * @param mxEvent The message event being rendered. + * @returns The registered renderer. + */ private selectRenderer(mxEvent: MatrixEvent): EventRenderer | undefined { return this.registeredMessageRenderers.find((rdr) => { if (typeof rdr.eventTypeOrFilter === "string") { @@ -72,9 +76,8 @@ export class CustomComponentsApi implements ICustomComponentsApi { } /** - * Get hints about a message before rendering it. - * @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. + * Get hints about an message before rendering it. + * @param mxEvent The message event being rendered. * @returns A component if a custom renderer exists, or originalComponent returns a value. Otherwise null. */ public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints { From ff986e43174e83ae7b5b5da35e79190b78d23ea1 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 16 Jun 2025 12:50:53 +0100 Subject: [PATCH 14/27] update @element-hq/element-web-module-api to 1.1.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4c15e5c7e72..33c2007e918 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@element-hq/element-web-module-api": "1.0.0", + "@element-hq/element-web-module-api": "1.1.0", "@fontsource/inconsolata": "^5", "@fontsource/inter": "^5", "@formatjs/intl-segmenter": "^11.5.7", diff --git a/yarn.lock b/yarn.lock index 4a9c8c9fd55..5fdfbb7f7ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1672,10 +1672,10 @@ resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.12.2.tgz#b6b6b7df69369b3088960b79591ce1bfd2b84a1a" integrity sha512-2u5/bOARcjc5TFq4929x1R0tvsNbeVA58FBtiW05GlIJCapxzPSOeeGhbqEcJ1TW3/hLGpiKMcw0QwRBQVNzQA== -"@element-hq/element-web-module-api@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.0.0.tgz#df09108b0346a44ad2898c603d1a6cda5f50d80b" - integrity sha512-FYId5tYgaKvpqAXRXqs0pY4+7/A09bEl1mCxFqlS9jlZOCjlMZVvZuv8spbY8ZN9HaMvuVmx9J00Fn2gCJd0TQ== +"@element-hq/element-web-module-api@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.1.0.tgz#ba45fbf7c3491437935b7bbcfc5677d4e1e66591" + integrity sha512-+DB+8MPFjslYwK/V4wIDUHib+0B/BB6d524zltWJVSjg4l3CVGL7PbXMi1m5pcbuuhOK6tpbxUmcfS7RgkpPrg== "@element-hq/element-web-playwright-common@^1.1.5": version "1.3.0" From 5aee224169638f821f4663ce2a1eed2ebfbab2b2 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 16 Jun 2025 13:43:27 +0100 Subject: [PATCH 15/27] fix types --- src/events/EventTileFactory.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 99c77fa172c..51bac059140 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -262,8 +262,6 @@ export function renderTile( return ModuleApi.customComponents.renderMessage( { mxEvent: props.mxEvent, - forExport: props.forExport, - highlights: props.highlights, }, undefined, ); @@ -297,8 +295,6 @@ export function renderTile( return ModuleApi.customComponents.renderMessage( { mxEvent: props.mxEvent, - forExport: props.forExport, - highlights: props.highlights, }, (origProps) => factory(props.ref, { @@ -318,8 +314,6 @@ export function renderTile( return ModuleApi.customComponents.renderMessage( { mxEvent: props.mxEvent, - forExport: props.forExport, - highlights: props.highlights, }, (origProps) => factory(ref, { @@ -361,8 +355,6 @@ export function renderReplyTile( return ModuleApi.customComponents.renderMessage( { mxEvent: props.mxEvent, - forExport: props.forExport, - highlights: props.highlights, }, undefined, ); @@ -386,8 +378,6 @@ export function renderReplyTile( return ModuleApi.customComponents.renderMessage( { mxEvent: props.mxEvent, - forExport: props.forExport, - highlights: props.highlights, }, (origProps) => factory(ref, { From 42edbab71526448fca066a5dfc8d5bee0f4bd965 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 16 Jun 2025 13:46:03 +0100 Subject: [PATCH 16/27] updates --- ...-component-crash-handle-renderer-linux.png | Bin 5730 -> 4772 bytes .../custom-component-tile-linux.png | Bin 6174 -> 7135 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-crash-handle-renderer-linux.png index e7d4241dca8b3bf0f10d251d470a7297cd0bd84f..b3fa5e0f57ddb478c2e3bf9d5b398db7687ba01d 100644 GIT binary patch literal 4772 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 diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png index 7c5d6b66e6d2d753dbeee10cceba0445dd1e6782..dabb11cbb4918bbda063ef2e7908bb1e86b362c6 100644 GIT binary patch literal 7135 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 Date: Mon, 16 Jun 2025 13:51:09 +0100 Subject: [PATCH 17/27] hide jump to bottom button that was causing flakes --- .../e2e/modules/custom-component.spec.ts | 67 ++++++------------ .../custom-component-tile-linux.png | Bin 7135 -> 6174 bytes 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/playwright/e2e/modules/custom-component.spec.ts b/playwright/e2e/modules/custom-component.spec.ts index 8299c0de118..f9c1e379d14 100644 --- a/playwright/e2e/modules/custom-component.spec.ts +++ b/playwright/e2e/modules/custom-component.spec.ts @@ -5,8 +5,22 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; +const screenshotOptions = (page: Page) => ({ + mask: [page.locator(".mx_MessageTimestamp")], + // Hide the jump to bottom button in the timeline to avoid flakiness + // Exclude timestamp and read marker from snapshot + css: ` + .mx_JumpToBottomButton { + display: none !important; + } + .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { + display: none !important; + } + `, +}); test.describe("Custom Component API", () => { test.use({ displayName: "Manny", @@ -31,15 +45,10 @@ test.describe("Custom Component API", () => { async ({ page, room, app }) => { await app.viewRoomById(room.roomId); await app.client.sendMessage(room.roomId, "Simple message"); - await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot("custom-component-tile.png", { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` - .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { - display: none !important; - } - `, - }); + await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( + "custom-component-tile.png", + screenshotOptions(page), + ); }, ); test( @@ -50,15 +59,7 @@ test.describe("Custom Component API", () => { await app.client.sendMessage(room.roomId, "Fall through here"); await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( "custom-component-tile-fall-through.png", - { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` - .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { - display: none !important; - } - `, - }, + screenshotOptions(page), ); }, ); @@ -70,15 +71,7 @@ test.describe("Custom Component API", () => { await app.client.sendMessage(room.roomId, "Do not replace me"); await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( "custom-component-tile-original.png", - { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` - .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { - display: none !important; - } - `, - }, + screenshotOptions(page), ); }, ); @@ -98,15 +91,7 @@ test.describe("Custom Component API", () => { await app.client.sendMessage(room.roomId, "Crash the filter!"); await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( "custom-component-crash-handle-filter.png", - { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` - .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { - display: none !important; - } - `, - }, + screenshotOptions(page), ); }, ); @@ -118,15 +103,7 @@ test.describe("Custom Component API", () => { await app.client.sendMessage(room.roomId, "Crash the renderer!"); await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot( "custom-component-crash-handle-renderer.png", - { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` - .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker { - display: none !important; - } - `, - }, + screenshotOptions(page), ); }, ); diff --git a/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png b/playwright/snapshots/modules/custom-component.spec.ts/custom-component-tile-linux.png index dabb11cbb4918bbda063ef2e7908bb1e86b362c6..7c5d6b66e6d2d753dbeee10cceba0445dd1e6782 100644 GIT binary patch literal 6174 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 Date: Tue, 17 Jun 2025 09:22:13 +0100 Subject: [PATCH 18/27] lint --- playwright/e2e/modules/custom-component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright/e2e/modules/custom-component.spec.ts b/playwright/e2e/modules/custom-component.spec.ts index f9c1e379d14..b0cdfe5855d 100644 --- a/playwright/e2e/modules/custom-component.spec.ts +++ b/playwright/e2e/modules/custom-component.spec.ts @@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { type Page } from "@playwright/test"; + import { test, expect } from "../../element-web-test"; const screenshotOptions = (page: Page) => ({ From d97d999ef26d886efbee6fcdba72a624c86fa1fb Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 17 Jun 2025 12:12:58 +0100 Subject: [PATCH 19/27] lint --- src/modules/customComponentApi.tsx | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index 71c0fc1a637..e8eb8279436 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -11,23 +11,35 @@ import { logger } from "matrix-js-sdk/src/logger"; import type { CustomComponentsApi as ICustomComponentsApi, CustomMessageRenderFunction, - CustomMessageComponentProps, + CustomMessageComponentProps as ModuleCustomMessageComponentProps, OriginalComponentProps, CustomMessageRenderHints, } from "@element-hq/element-web-module-api"; import type React from "react"; +// element-modules uses the compiled MatrixEvent interface from matrix-js-sdk +// whereas element-web uses MatrixEvent from the source. Due to the private +// fields conflicting, this type is used to stip MatrixEvent down to the public +// fields only which should match. +type MatrixEventPublic = Pick; + type EventRenderer = { - eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean); + eventTypeOrFilter: string | ((mxEvent: MatrixEventPublic) => boolean); renderer: CustomMessageRenderFunction; hints: CustomMessageRenderHints; }; +// As per MatrixEventPublic, this type overrides `ModuleCustomMessageComponentProps` to use +// our stripped type. +interface CustomMessageComponentProps extends Omit { + mxEvent: MatrixEventPublic; +} + export class CustomComponentsApi implements ICustomComponentsApi { private readonly registeredMessageRenderers: EventRenderer[] = []; public registerMessageRenderer( - eventTypeOrFilter: string | ((mxEvent: MatrixEvent) => boolean), + eventTypeOrFilter: string | ((mxEvent: MatrixEventPublic) => boolean), renderer: CustomMessageRenderFunction, hints: CustomMessageRenderHints = {}, ): void { @@ -38,7 +50,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { * @param mxEvent The message event being rendered. * @returns The registered renderer. */ - private selectRenderer(mxEvent: MatrixEvent): EventRenderer | undefined { + private selectRenderer(mxEvent: MatrixEventPublic): EventRenderer | undefined { return this.registeredMessageRenderers.find((rdr) => { if (typeof rdr.eventTypeOrFilter === "string") { return rdr.eventTypeOrFilter === mxEvent.getType(); @@ -66,7 +78,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { const renderer = this.selectRenderer(props.mxEvent); if (renderer) { try { - return renderer.renderer(props, originalComponent); + return renderer.renderer(props as ModuleCustomMessageComponentProps, originalComponent); } catch (ex) { logger.warn("Message renderer failed to render", ex); // Fall through to original component. If the module encounters an error we still want to display messages to the user! @@ -80,7 +92,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { * @param mxEvent The message event being rendered. * @returns A component if a custom renderer exists, or originalComponent returns a value. Otherwise null. */ - public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints { + public getHintsForMessage(mxEvent: MatrixEventPublic): CustomMessageRenderHints { const renderer = this.selectRenderer(mxEvent); if (renderer) { return renderer.hints; From 66e73818a8ce03b9224053f35fa1b331db380c31 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 17 Jun 2025 13:34:29 +0100 Subject: [PATCH 20/27] Use module matrix event interface instead. --- src/modules/customComponentApi.tsx | 57 +++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index e8eb8279436..3c26fb63004 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -14,32 +14,53 @@ import type { CustomMessageComponentProps as ModuleCustomMessageComponentProps, OriginalComponentProps, CustomMessageRenderHints, + MatrixEvent as ModuleMatrixEvent, } from "@element-hq/element-web-module-api"; import type React from "react"; -// element-modules uses the compiled MatrixEvent interface from matrix-js-sdk -// whereas element-web uses MatrixEvent from the source. Due to the private -// fields conflicting, this type is used to stip MatrixEvent down to the public -// fields only which should match. -type MatrixEventPublic = Pick; - type EventRenderer = { - eventTypeOrFilter: string | ((mxEvent: MatrixEventPublic) => boolean); + eventTypeOrFilter: string | ((mxEvent: ModuleMatrixEvent) => boolean); renderer: CustomMessageRenderFunction; hints: CustomMessageRenderHints; }; -// As per MatrixEventPublic, this type overrides `ModuleCustomMessageComponentProps` to use -// our stripped type. interface CustomMessageComponentProps extends Omit { - mxEvent: MatrixEventPublic; + mxEvent: MatrixEvent; } export class CustomComponentsApi implements ICustomComponentsApi { + /** + * Convert a matrix-js-sdk event into a SDK-neutral ModuleMatrixEvent. + * @param sdkEvent + * @returns An event object, or `null` if the event was not a message event. + */ + private static getModuleMatrixEvent(sdkEvent: MatrixEvent): ModuleMatrixEvent | null { + const eventId = sdkEvent.getId(); + const roomId = sdkEvent.getRoomId(); + const sender = sdkEvent.sender; + // Typically we wouldn't expect messages without these keys to be rendered + // by the timeline, but for the sake of type safety. + if (!eventId || !roomId || !sender) { + // Not a message event. + return null; + } + return { + content: sdkEvent.getContent(), + eventId, + originServerTs: sdkEvent.getTs(), + redacts: sdkEvent.event.redacts, + roomId, + sender: sender.userId, + stateKey: sdkEvent.getStateKey(), + type: sdkEvent.getType(), + unsigned: sdkEvent.getUnsigned(), + }; + } + private readonly registeredMessageRenderers: EventRenderer[] = []; public registerMessageRenderer( - eventTypeOrFilter: string | ((mxEvent: MatrixEventPublic) => boolean), + eventTypeOrFilter: string | ((mxEvent: ModuleMatrixEvent) => boolean), renderer: CustomMessageRenderFunction, hints: CustomMessageRenderHints = {}, ): void { @@ -50,10 +71,10 @@ export class CustomComponentsApi implements ICustomComponentsApi { * @param mxEvent The message event being rendered. * @returns The registered renderer. */ - private selectRenderer(mxEvent: MatrixEventPublic): EventRenderer | undefined { + private selectRenderer(mxEvent: ModuleMatrixEvent): EventRenderer | undefined { return this.registeredMessageRenderers.find((rdr) => { if (typeof rdr.eventTypeOrFilter === "string") { - return rdr.eventTypeOrFilter === mxEvent.getType(); + return rdr.eventTypeOrFilter === mxEvent.type; } else { try { return rdr.eventTypeOrFilter(mxEvent); @@ -75,10 +96,11 @@ export class CustomComponentsApi implements ICustomComponentsApi { props: CustomMessageComponentProps, originalComponent?: (props?: OriginalComponentProps) => React.JSX.Element, ): React.JSX.Element | null { - const renderer = this.selectRenderer(props.mxEvent); + const moduleEv = CustomComponentsApi.getModuleMatrixEvent(props.mxEvent); + const renderer = moduleEv && this.selectRenderer(moduleEv); if (renderer) { try { - return renderer.renderer(props as ModuleCustomMessageComponentProps, originalComponent); + return renderer.renderer({ ...props, mxEvent: moduleEv }, originalComponent); } catch (ex) { logger.warn("Message renderer failed to render", ex); // Fall through to original component. If the module encounters an error we still want to display messages to the user! @@ -92,8 +114,9 @@ export class CustomComponentsApi implements ICustomComponentsApi { * @param mxEvent The message event being rendered. * @returns A component if a custom renderer exists, or originalComponent returns a value. Otherwise null. */ - public getHintsForMessage(mxEvent: MatrixEventPublic): CustomMessageRenderHints { - const renderer = this.selectRenderer(mxEvent); + public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints { + const moduleEv = CustomComponentsApi.getModuleMatrixEvent(mxEvent); + const renderer = moduleEv && this.selectRenderer(moduleEv); if (renderer) { return renderer.hints; } From 706b33fcf4e5fa6db033c9fe802267634dd230f9 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 17 Jun 2025 16:02:44 +0100 Subject: [PATCH 21/27] update to new module sdk --- package.json | 2 +- src/modules/customComponentApi.tsx | 23 +++++++++++------------ yarn.lock | 8 ++++---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 33c2007e918..7d1dd907c82 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@element-hq/element-web-module-api": "1.1.0", + "@element-hq/element-web-module-api": "1.2.0", "@fontsource/inconsolata": "^5", "@fontsource/inter": "^5", "@formatjs/intl-segmenter": "^11.5.7", diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index 3c26fb63004..2daa788afeb 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -30,14 +30,14 @@ interface CustomMessageComponentProps extends Omit Date: Tue, 17 Jun 2025 16:16:06 +0100 Subject: [PATCH 22/27] adapt custom module sample --- playwright/sample-files/custom-component-module.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/playwright/sample-files/custom-component-module.js b/playwright/sample-files/custom-component-module.js index 563b4123c5d..8d4d1b3c1f6 100644 --- a/playwright/sample-files/custom-component-module.js +++ b/playwright/sample-files/custom-component-module.js @@ -6,26 +6,26 @@ Please see LICENSE files in the repository root for full details. */ export default class CustomComponentModule { - static moduleApiVersion = "^1.0.0"; + static moduleApiVersion = "^1.2.0"; constructor(api) { this.api = api; this.api.customComponents.registerMessageRenderer( - (evt) => evt.getContent().body === "Do not show edits", + (evt) => evt.content.body === "Do not show edits", (_props, originalComponent) => { return originalComponent(); }, { allowEditingEvent: false }, ); this.api.customComponents.registerMessageRenderer( - (evt) => evt.getContent().body === "Fall through here", + (evt) => evt.content.body === "Fall through here", (props) => { - const body = props.mxEvent.getContent().body; + const body = props.mxEvent.content.body; return `Fallthrough text for ${body}`; }, ); this.api.customComponents.registerMessageRenderer( (evt) => { - if (evt.getContent().body === "Crash the filter!") { + if (evt.content.body === "Crash the filter!") { throw new Error("Fail test!"); } return false; @@ -35,14 +35,14 @@ export default class CustomComponentModule { }, ); this.api.customComponents.registerMessageRenderer( - (evt) => evt.getContent().body === "Crash the renderer!", + (evt) => evt.content.body === "Crash the renderer!", () => { throw new Error("Fail test!"); }, ); // Order is specific here to avoid this overriding the other renderers this.api.customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => { - const body = props.mxEvent.getContent().body; + const body = props.mxEvent.content.body; if (body === "Do not replace me") { return originalComponent(); } else if (body === "Fall through here") { From 6d2f1c2e9ad9c98608cf9438b9c9a7083814419c Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 19 Jun 2025 08:26:39 +0100 Subject: [PATCH 23/27] Issues caught by Sonar --- src/events/EventTileFactory.tsx | 6 ++---- src/modules/customComponentApi.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 51bac059140..765a10e8d8a 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -262,8 +262,7 @@ export function renderTile( return ModuleApi.customComponents.renderMessage( { mxEvent: props.mxEvent, - }, - undefined, + } ); // Note that we split off the ones we actually care about here just to be sure that we're @@ -355,8 +354,7 @@ export function renderReplyTile( return ModuleApi.customComponents.renderMessage( { mxEvent: props.mxEvent, - }, - undefined, + } ); // See renderTile() for why we split off so much diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.tsx index 2daa788afeb..fdeebd8903f 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.tsx @@ -105,7 +105,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { // Fall through to original component. If the module encounters an error we still want to display messages to the user! } } - return originalComponent?.() || null; + return originalComponent?.() ?? null; } /** From 9eb5e8965af69d3a029707e8b5fe246696e7b858 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 19 Jun 2025 10:16:11 +0100 Subject: [PATCH 24/27] lint --- src/events/EventTileFactory.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 765a10e8d8a..b7edc4d1a7e 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -259,11 +259,9 @@ export function renderTile( const factory = pickFactory(props.mxEvent, cli, showHiddenEvents); if (!factory) - return ModuleApi.customComponents.renderMessage( - { - mxEvent: props.mxEvent, - } - ); + return ModuleApi.customComponents.renderMessage({ + mxEvent: props.mxEvent, + }); // Note that we split off the ones we actually care about here just to be sure that we're // not going to accidentally send things we shouldn't from lazy callers. Eg: EventTile's @@ -351,11 +349,9 @@ export function renderReplyTile( const factory = pickFactory(props.mxEvent, cli, showHiddenEvents); if (!factory) - return ModuleApi.customComponents.renderMessage( - { - mxEvent: props.mxEvent, - } - ); + return ModuleApi.customComponents.renderMessage({ + mxEvent: props.mxEvent, + }); // See renderTile() for why we split off so much const { From 9f7359e0a4bc8ee41e723e5623036fbfd23fd847 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 23 Jun 2025 09:25:30 +0100 Subject: [PATCH 25/27] fix issues --- src/events/EventTileFactory.tsx | 19 ++++++++++++++++--- ...ComponentApi.tsx => customComponentApi.ts} | 18 ++++++++++-------- src/utils/EventUtils.ts | 2 +- 3 files changed, 27 insertions(+), 12 deletions(-) rename src/modules/{customComponentApi.tsx => customComponentApi.ts} (90%) diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index b7edc4d1a7e..dc77ffddea5 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -258,10 +258,14 @@ export function renderTile( cli = cli ?? MatrixClientPeg.safeGet(); // because param defaults don't do the correct thing const factory = pickFactory(props.mxEvent, cli, showHiddenEvents); - if (!factory) + if (!factory) { + // If we don't have a factory for this event, attempt + // to find a custom component that can render it. + // Will return null if no custom component can render it. return ModuleApi.customComponents.renderMessage({ mxEvent: props.mxEvent, }); + } // Note that we split off the ones we actually care about here just to be sure that we're // not going to accidentally send things we shouldn't from lazy callers. Eg: EventTile's @@ -288,13 +292,13 @@ export function renderTile( case TimelineRenderingType.File: case TimelineRenderingType.Notification: case TimelineRenderingType.Thread: - // We only want a subset of props, so we don't end up causing issues for downstream components. return ModuleApi.customComponents.renderMessage( { mxEvent: props.mxEvent, }, (origProps) => factory(props.ref, { + // We only want a subset of props, so we don't end up causing issues for downstream components. mxEvent, highlights, highlightLink, @@ -348,10 +352,14 @@ export function renderReplyTile( cli = cli ?? MatrixClientPeg.safeGet(); // because param defaults don't do the correct thing const factory = pickFactory(props.mxEvent, cli, showHiddenEvents); - if (!factory) + if (!factory) { + // If we don't have a factory for this event, attempt + // to find a custom component that can render it. + // Will return null if no custom component can render it. return ModuleApi.customComponents.renderMessage({ mxEvent: props.mxEvent, }); + } // See renderTile() for why we split off so much const { @@ -411,6 +419,11 @@ export function haveRendererForEvent( return false; } + // Check to see if a renderer is registered for this event + if (ModuleApi.customComponents.getHintsForMessage(mxEvent)) { + return true; + } + // No tile for replacement events since they update the original tile if (mxEvent.isRelation(RelationType.Replace)) return false; diff --git a/src/modules/customComponentApi.tsx b/src/modules/customComponentApi.ts similarity index 90% rename from src/modules/customComponentApi.tsx rename to src/modules/customComponentApi.ts index fdeebd8903f..ce75a705071 100644 --- a/src/modules/customComponentApi.tsx +++ b/src/modules/customComponentApi.ts @@ -18,8 +18,10 @@ import type { } from "@element-hq/element-web-module-api"; import type React from "react"; +type EventTypeOrFilter = Parameters[0]; + type EventRenderer = { - eventTypeOrFilter: string | ((mxEvent: ModuleMatrixEvent) => boolean); + eventTypeOrFilter: EventTypeOrFilter; renderer: CustomMessageRenderFunction; hints: CustomMessageRenderHints; }; @@ -59,7 +61,7 @@ export class CustomComponentsApi implements ICustomComponentsApi { private readonly registeredMessageRenderers: EventRenderer[] = []; public registerMessageRenderer( - eventTypeOrFilter: string | ((mxEvent: ModuleMatrixEvent) => boolean), + eventTypeOrFilter: EventTypeOrFilter, renderer: CustomMessageRenderFunction, hints: CustomMessageRenderHints = {}, ): void { @@ -71,12 +73,12 @@ export class CustomComponentsApi implements ICustomComponentsApi { * @returns The registered renderer. */ private selectRenderer(mxEvent: ModuleMatrixEvent): EventRenderer | undefined { - return this.registeredMessageRenderers.find((rdr) => { - if (typeof rdr.eventTypeOrFilter === "string") { - return rdr.eventTypeOrFilter === mxEvent.type; + return this.registeredMessageRenderers.find((renderer) => { + if (typeof renderer.eventTypeOrFilter === "string") { + return renderer.eventTypeOrFilter === mxEvent.type; } else { try { - return rdr.eventTypeOrFilter(mxEvent); + return renderer.eventTypeOrFilter(mxEvent); } catch (ex) { logger.warn("Message renderer failed to process filter", ex); return false; // Skip erroring renderers. @@ -113,12 +115,12 @@ export class CustomComponentsApi implements ICustomComponentsApi { * @param mxEvent The message event being rendered. * @returns A component if a custom renderer exists, or originalComponent returns a value. Otherwise null. */ - public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints { + public getHintsForMessage(mxEvent: MatrixEvent): CustomMessageRenderHints | null { const moduleEv = CustomComponentsApi.getModuleMatrixEvent(mxEvent); const renderer = moduleEv && this.selectRenderer(moduleEv); if (renderer) { return renderer.hints; } - return {}; + return null; } } diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 73d4ffebec6..ecaa7e06ec2 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -78,7 +78,7 @@ export function canEditContent(matrixClient: MatrixClient, mxEvent: MatrixEvent) return false; } - if (ModuleApi.customComponents.getHintsForMessage(mxEvent).allowEditingEvent === false) { + if (ModuleApi.customComponents.getHintsForMessage(mxEvent)?.allowEditingEvent === false) { return false; } From 3f39741d22674d335a83675694bd084918c94e2c Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 23 Jun 2025 09:37:07 +0100 Subject: [PATCH 26/27] make the comment make sense --- src/events/EventTileFactory.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index dc77ffddea5..0d91ded160f 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -419,7 +419,8 @@ export function haveRendererForEvent( return false; } - // Check to see if a renderer is registered for this event + // Check to see if we have any hints for this message, which indicates + // there is a custom renderer for the event. if (ModuleApi.customComponents.getHintsForMessage(mxEvent)) { return true; } From bb0f2d65abd94edd62acb0e8c60bdcc28fb30027 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 23 Jun 2025 12:00:07 +0100 Subject: [PATCH 27/27] fix import --- src/modules/Api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Api.ts b/src/modules/Api.ts index a74a99282ec..db7dd803344 100644 --- a/src/modules/Api.ts +++ b/src/modules/Api.ts @@ -21,7 +21,7 @@ import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissi import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts"; import { ConfigApi } from "./ConfigApi.ts"; import { I18nApi } from "./I18nApi.ts"; -import { CustomComponentsApi } from "./customComponentApi.tsx"; +import { CustomComponentsApi } from "./customComponentApi.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false;