From 78d7c31242c97d95e366e4af541b64dba5164a9d Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 22 Jul 2026 18:48:17 +0200 Subject: [PATCH 1/5] feat(widget): Rtc transports discovery for widgets --- .../src/stores/widgets/ElementWidgetDriver.ts | 12 ++++++++ .../widgets/ElementWidgetDriver-test.ts | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/apps/web/src/stores/widgets/ElementWidgetDriver.ts b/apps/web/src/stores/widgets/ElementWidgetDriver.ts index 951baefa65d..87bc21e68f7 100644 --- a/apps/web/src/stores/widgets/ElementWidgetDriver.ts +++ b/apps/web/src/stores/widgets/ElementWidgetDriver.ts @@ -26,6 +26,7 @@ import { type IWidgetApiErrorResponseDataDetails, type ISearchUserDirectoryResult, type IGetMediaConfigResult, + type IRtcTransportsResult, } from "matrix-widget-api"; import { ClientEvent, @@ -123,6 +124,7 @@ export class ElementWidgetDriver extends WidgetDriver { // This is a trusted Element Call widget that we control this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen); this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers); + this.allowedCapabilities.add(MatrixCapabilities.MSC4515RtcTransports); this.allowedCapabilities.add(`org.matrix.msc2762.timeline:${inRoomId}`); this.allowedCapabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent); this.allowedCapabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent); @@ -765,6 +767,16 @@ export class ElementWidgetDriver extends WidgetDriver { } } + public async getRtcTransports(): Promise { + const client = MatrixClientPeg.safeGet(); + // Delegate to the authenticated CS endpoint (MSC4143). Any error (e.g. the + // homeserver not supporting it) propagates and is turned into a widget error + // response by ClientWidgetApi. The js-sdk Transport and widget-api IRtcTransport + // types are structurally identical. + const transports = await client._unstable_getRTCTransports(); + return { rtc_transports: transports }; + } + public async readEventRelations( eventId: string, roomId?: string, diff --git a/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts b/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts index e7d8af86e31..10c9c01a245 100644 --- a/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts +++ b/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts @@ -14,6 +14,7 @@ import { type ITurnServer as IClientTurnServer, Direction, EventType, + MatrixError, MatrixEvent, MsgType, RelationType, @@ -89,6 +90,7 @@ describe("ElementWidgetDriver", () => { const requestedCapabilities = new Set([ "m.always_on_screen", "town.robin.msc3846.turn_servers", + "org.matrix.msc4515.rtc_transports", "org.matrix.msc2762.timeline:!1:example.org", "org.matrix.msc2762.send.event:org.matrix.msc4075.call.notify", "org.matrix.msc2762.send.event:org.matrix.msc4075.rtc.notification", @@ -420,6 +422,34 @@ describe("ElementWidgetDriver", () => { }); }); + describe("getRtcTransports", () => { + let driver: WidgetDriver; + + beforeEach(() => { + driver = mkDefaultDriver(); + }); + + it("gets the RTC transports from the homeserver", async () => { + const transports = [{ type: "livekit", livekit_service_url: "https://livekit-jwt.example.com" }]; + client._unstable_getRTCTransports.mockResolvedValue(transports); + + await expect(driver.getRtcTransports()).resolves.toEqual({ rtc_transports: transports }); + + expect(client._unstable_getRTCTransports).toHaveBeenCalledWith(); + }); + + it("propagates errors from the homeserver", async () => { + const error = new MatrixError( + { errcode: "M_NOT_FOUND", error: "Not found" }, + 404, + ); + + client._unstable_getRTCTransports.mockRejectedValue(error); + + await expect(driver.getRtcTransports()).rejects.toBe(error); + }); + }); + describe("readEventRelations", () => { let driver: WidgetDriver; From afe7754ccd9cb9bdf717ef5d2fbe823e36d7b984 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 30 Jul 2026 10:12:38 +0200 Subject: [PATCH 2/5] fix linter --- .../unit-tests/stores/widgets/ElementWidgetDriver-test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts b/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts index 10c9c01a245..30ee0e9b5ce 100644 --- a/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts +++ b/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts @@ -439,10 +439,7 @@ describe("ElementWidgetDriver", () => { }); it("propagates errors from the homeserver", async () => { - const error = new MatrixError( - { errcode: "M_NOT_FOUND", error: "Not found" }, - 404, - ); + const error = new MatrixError({ errcode: "M_NOT_FOUND", error: "Not found" }, 404); client._unstable_getRTCTransports.mockRejectedValue(error); From 78fe92ec0013047ab1d179c9f6cd1fb6a8307f70 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 30 Jul 2026 10:23:30 +0200 Subject: [PATCH 3/5] update matrix-widget-api and js-sdk --- apps/web/package.json | 2 +- pnpm-lock.yaml | 36 ++++++++++++++++++------------------ pnpm-workspace.yaml | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 589aca8b2e5..e9e9aed3c7d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -82,7 +82,7 @@ "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^1.16.1", + "matrix-widget-api": "^1.18.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", "opus-recorder": "^8.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5945d4403f..dafa60578c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,7 +273,7 @@ overrides: '@types/react': ^19.2.10 '@types/react-dom': ^19.2.3 caniuse-lite: 1.0.30001806 - matrix-widget-api: ^1.17.0 + matrix-widget-api: ^1.18.0 '@types/node': 25.9.3 config-file-ts: 0.2.8-rc1 node-abi: 4.33.0 @@ -642,10 +642,10 @@ importers: version: 1.0.3 matrix-js-sdk: specifier: github:matrix-org/matrix-js-sdk#develop - version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/191908da06709ada601264993734294de86999d6 + version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0d8558de2c682a8cd719d385bfa0b5c01f2fb69e matrix-widget-api: - specifier: ^1.17.0 - version: 1.17.0 + specifier: ^1.18.0 + version: 1.18.0 memoize-one: specifier: ^6.0.0 version: 6.0.0 @@ -1102,8 +1102,8 @@ importers: specifier: ^3.6.0 version: 3.6.0 matrix-widget-api: - specifier: ^1.17.0 - version: 1.17.0 + specifier: ^1.18.0 + version: 1.18.0 react: specifier: 'catalog:' version: 19.2.8 @@ -1197,8 +1197,8 @@ importers: specifier: ^9.0.0 version: 9.9.0(@fontsource/inconsolata@5.2.8)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3)(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.1)(react-dom@19.2.8)(react@19.2.8) matrix-widget-api: - specifier: ^1.17.0 - version: 1.17.0 + specifier: ^1.18.0 + version: 1.18.0 styled-components: specifier: ^6.3.11 version: 6.4.4(react-dom@19.2.8)(react@19.2.8) @@ -1283,8 +1283,8 @@ importers: specifier: 'catalog:' version: typescript@7.0.2 matrix-widget-api: - specifier: ^1.17.0 - version: 1.17.0 + specifier: ^1.18.0 + version: 1.18.0 rollup-plugin-external-globals: specifier: ^0.13.0 version: 0.13.0(rollup@4.60.1) @@ -8697,7 +8697,7 @@ packages: engines: {node: '>=12'} eslint-plugin-element-call@https://codeload.github.com/element-hq/element-call/tar.gz/62eeb10d382a7386953e02f12e3bf87031148ab1#path:/eslint: - resolution: {gitHosted: true, path: /eslint, integrity: sha512-IFSuCAiIua6NGcaPUuRwl7f+NlBCgRbjqU2IZO/nNuwc+T75GDgH3QwdMPQWkr9R4SYXTz06H/Ihq5btMMKXVw==, tarball: https://codeload.github.com/element-hq/element-call/tar.gz/62eeb10d382a7386953e02f12e3bf87031148ab1} + resolution: {gitHosted: true, integrity: sha512-IFSuCAiIua6NGcaPUuRwl7f+NlBCgRbjqU2IZO/nNuwc+T75GDgH3QwdMPQWkr9R4SYXTz06H/Ihq5btMMKXVw==, tarball: https://codeload.github.com/element-hq/element-call/tar.gz/62eeb10d382a7386953e02f12e3bf87031148ab1} version: 0.0.0 eslint-plugin-storybook@10.5.4: @@ -10265,8 +10265,8 @@ packages: matrix-events-sdk@0.0.1: resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/191908da06709ada601264993734294de86999d6: - resolution: {gitHosted: true, integrity: sha512-d+mtpj4IwF+DC6yM7UcVUZk+4W9iut0AVakC14v/essqHENK+J8V4+bPAfX7pIXHrHiThAXRttzprOrjK2q8XQ==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/191908da06709ada601264993734294de86999d6} + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0d8558de2c682a8cd719d385bfa0b5c01f2fb69e: + resolution: {gitHosted: true, integrity: sha512-FUb/VMRdWip97nlZJTgvL5wAGTefBxXAtU0+F8VTGgqMNFUyVX5CtGiLZbu20f4COsvc1tYuKNoSK7aMAvApsg==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0d8558de2c682a8cd719d385bfa0b5c01f2fb69e} version: 42.0.0 engines: {node: '>=22.0.0'} @@ -10274,8 +10274,8 @@ packages: resolution: {integrity: sha512-oY4YZCU8OqDZzmCiJpo8XrATa7XNOB5oFyIpE7O0jjaIEeu6LnQrHmm/fpsYkPUENVzGJm1YdoOtvcBrQx+h2g==} hasBin: true - matrix-widget-api@1.17.0: - resolution: {integrity: sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==} + matrix-widget-api@1.18.0: + resolution: {integrity: sha512-4T2f2koWmx05p1BLcT/9YGGGPSXpPT+PA4Oap/5fjhXsWPxMGJiGL97YpANMYLKsnE1sScYFeuyxIgdc1Qo+Ew==} md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} @@ -23065,7 +23065,7 @@ snapshots: matrix-events-sdk@0.0.1: {} - matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/191908da06709ada601264993734294de86999d6: + matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0d8558de2c682a8cd719d385bfa0b5c01f2fb69e: dependencies: '@babel/runtime': 8.0.0 '@matrix-org/matrix-sdk-crypto-wasm': 18.4.0 @@ -23074,7 +23074,7 @@ snapshots: content-type: 2.0.0 loglevel: 1.9.2 matrix-events-sdk: 0.0.1 - matrix-widget-api: 1.17.0 + matrix-widget-api: 1.18.0 p-retry: 8.0.0 sdp-transform: 3.0.0 unhomoglyph: 1.0.6 @@ -23089,7 +23089,7 @@ snapshots: transitivePeerDependencies: - supports-color - matrix-widget-api@1.17.0: + matrix-widget-api@1.18.0: dependencies: '@types/events': 3.0.3 events: 3.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8beca92cf9c..bfb2323580b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -125,7 +125,7 @@ overrides: "@types/react": "catalog:" "@types/react-dom": "catalog:" caniuse-lite: 1.0.30001806 - matrix-widget-api: "^1.17.0" + matrix-widget-api: "^1.18.0" "@types/node": 25.9.3 config-file-ts: 0.2.8-rc1 node-abi: 4.33.0 From 6a9b02b682455c7f0d661570874c1144c9edbcbb Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 30 Jul 2026 10:50:09 +0200 Subject: [PATCH 4/5] fix eslint-plugin-element-call path --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dafa60578c4..d8c4cacf020 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8697,7 +8697,7 @@ packages: engines: {node: '>=12'} eslint-plugin-element-call@https://codeload.github.com/element-hq/element-call/tar.gz/62eeb10d382a7386953e02f12e3bf87031148ab1#path:/eslint: - resolution: {gitHosted: true, integrity: sha512-IFSuCAiIua6NGcaPUuRwl7f+NlBCgRbjqU2IZO/nNuwc+T75GDgH3QwdMPQWkr9R4SYXTz06H/Ihq5btMMKXVw==, tarball: https://codeload.github.com/element-hq/element-call/tar.gz/62eeb10d382a7386953e02f12e3bf87031148ab1} + resolution: {gitHosted: true, path: /eslint, integrity: sha512-IFSuCAiIua6NGcaPUuRwl7f+NlBCgRbjqU2IZO/nNuwc+T75GDgH3QwdMPQWkr9R4SYXTz06H/Ihq5btMMKXVw==, tarball: https://codeload.github.com/element-hq/element-call/tar.gz/62eeb10d382a7386953e02f12e3bf87031148ab1} version: 0.0.0 eslint-plugin-storybook@10.5.4: From edec315b1d615ef7f81747fbbb53c37bbba1ef5f Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 30 Jul 2026 14:56:20 +0200 Subject: [PATCH 5/5] update widget-api: Parent-id delegation is deprectated --- .../src/stores/widgets/ElementWidgetDriver.ts | 33 ++------- .../widgets/ElementWidgetDriver-test.ts | 71 +------------------ 2 files changed, 9 insertions(+), 95 deletions(-) diff --git a/apps/web/src/stores/widgets/ElementWidgetDriver.ts b/apps/web/src/stores/widgets/ElementWidgetDriver.ts index 87bc21e68f7..6de3a30aadf 100644 --- a/apps/web/src/stores/widgets/ElementWidgetDriver.ts +++ b/apps/web/src/stores/widgets/ElementWidgetDriver.ts @@ -40,7 +40,6 @@ import { type StateEvents, type TimelineEvents, type Room, - type SendDelayedEventRequestOpts, type MatrixClient, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; @@ -407,27 +406,12 @@ export class ElementWidgetDriver extends WidgetDriver { return stickyEvents; } - private getSendDelayedEventOpts(delay: number | null, parentDelayId: string | null): SendDelayedEventRequestOpts { - if (delay !== null) { - return { - delay, - ...(parentDelayId !== null && { parent_delay_id: parentDelayId }), - }; - } else if (parentDelayId !== null) { - return { - parent_delay_id: parentDelayId, - }; - } - throw new Error("Must provide at least one of delay or parentDelayId"); - } - /** * @experimental Part of MSC4140 & MSC4157 * @see {@link WidgetDriver#sendDelayedEvent} */ public async sendDelayedEvent( - delay: number | null, - parentDelayId: string | null, + delay: number, eventType: K, content: StateEvents[K], stateKey: string | null, @@ -437,23 +421,21 @@ export class ElementWidgetDriver extends WidgetDriver { * @experimental Part of MSC4140 & MSC4157 */ public async sendDelayedEvent( - delay: number | null, - parentDelayId: string | null, + delay: number, eventType: K, content: TimelineEvents[K], stateKey: null, targetRoomId: string | null, ): Promise; public async sendDelayedEvent( - delay: number | null, - parentDelayId: string | null, + delay: number, eventType: string, content: IContent, stateKey: string | null = null, targetRoomId: string | null = null, ): Promise { const { client, roomId } = this.getSendEventTarget(targetRoomId); - const delayOpts = this.getSendDelayedEventOpts(delay, parentDelayId); + const delayOpts = { delay }; let r: SendDelayedEventResponse | null; if (stateKey !== null) { @@ -487,20 +469,17 @@ export class ElementWidgetDriver extends WidgetDriver { * @see {@link WidgetDriver#sendStickyEvent} */ public async sendDelayedStickyEvent( - delay: number | null, - parentDelayId: string | null, + delay: number, stickyDurationMs: number, eventType: string, content: unknown, targetRoomId?: string | null, ): Promise { const { client, roomId } = this.getSendEventTarget(targetRoomId); - const delayOpts = this.getSendDelayedEventOpts(delay, parentDelayId); - const r = await client._unstable_sendStickyDelayedEvent( roomId, stickyDurationMs, - delayOpts, + { delay }, null, eventType as keyof TimelineEvents, content as TimelineEvents[keyof TimelineEvents] & { msc4354_sticky_key: string }, diff --git a/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts b/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts index 30ee0e9b5ce..2824dbc9c49 100644 --- a/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts +++ b/apps/web/test/unit-tests/stores/widgets/ElementWidgetDriver-test.ts @@ -567,18 +567,12 @@ describe("ElementWidgetDriver", () => { driver = mkDefaultDriver(); }); - it("cannot send delayed events with missing arguments", async () => { - await expect(driver.sendDelayedEvent(null, null, EventType.RoomMessage, {})).rejects.toThrow( - "Must provide at least one of", - ); - }); - it("sends delayed message events", async () => { client._unstable_sendDelayedEvent.mockResolvedValue({ delay_id: "id", }); - await expect(driver.sendDelayedEvent(2000, null, EventType.RoomMessage, {})).resolves.toEqual({ + await expect(driver.sendDelayedEvent(2000, EventType.RoomMessage, {})).resolves.toEqual({ roomId, delayId: "id", }); @@ -592,31 +586,12 @@ describe("ElementWidgetDriver", () => { ); }); - it("sends child action delayed message events", async () => { - client._unstable_sendDelayedEvent.mockResolvedValue({ - delay_id: "id-child", - }); - - await expect(driver.sendDelayedEvent(null, "id-parent", EventType.RoomMessage, {})).resolves.toEqual({ - roomId, - delayId: "id-child", - }); - - expect(client._unstable_sendDelayedEvent).toHaveBeenCalledWith( - roomId, - { parent_delay_id: "id-parent" }, - null, - EventType.RoomMessage, - {}, - ); - }); - it("sends delayed state events", async () => { client._unstable_sendDelayedStateEvent.mockResolvedValue({ delay_id: "id", }); - await expect(driver.sendDelayedEvent(2000, null, EventType.RoomTopic, {}, "")).resolves.toEqual({ + await expect(driver.sendDelayedEvent(2000, EventType.RoomTopic, {}, "")).resolves.toEqual({ roomId, delayId: "id", }); @@ -629,25 +604,6 @@ describe("ElementWidgetDriver", () => { "", ); }); - - it("sends child action delayed state events", async () => { - client._unstable_sendDelayedStateEvent.mockResolvedValue({ - delay_id: "id-child", - }); - - await expect(driver.sendDelayedEvent(null, "id-parent", EventType.RoomTopic, {}, "")).resolves.toEqual({ - roomId, - delayId: "id-child", - }); - - expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith( - roomId, - { parent_delay_id: "id-parent" }, - EventType.RoomTopic, - {}, - "", - ); - }); }); describe("updateDelayedEvent", () => { @@ -770,7 +726,7 @@ describe("ElementWidgetDriver", () => { delay_id: "id", }); - await expect(driver.sendDelayedStickyEvent(1000, null, 2000, EventType.RoomMessage, {})).resolves.toEqual({ + await expect(driver.sendDelayedStickyEvent(1000, 2000, EventType.RoomMessage, {})).resolves.toEqual({ roomId, delayId: "id", }); @@ -784,27 +740,6 @@ describe("ElementWidgetDriver", () => { {}, ); }); - it("sends child action delayed sticky message events", async () => { - client._unstable_sendStickyDelayedEvent.mockResolvedValue({ - delay_id: "id-child", - }); - - await expect( - driver.sendDelayedStickyEvent(null, "id-parent", 2000, EventType.RoomMessage, {}), - ).resolves.toEqual({ - roomId, - delayId: "id-child", - }); - - expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledWith( - roomId, - 2000, - { parent_delay_id: "id-parent" }, - null, - EventType.RoomMessage, - {}, - ); - }); }); describe("If the feature_dynamic_room_predecessors feature is not enabled", () => {