diff --git a/locales/en/app.json b/locales/en/app.json index c70a0a49..0b0ac7b4 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -116,7 +116,7 @@ "matrix_rtc_transport_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).", "membership_manager": "Membership Manager Error", "membership_manager_description": "The Membership Manager had to shut down. This is caused by many consequtive failed network requests.", - "no_matrix_2_authorization_service": "Your authorization service for you media server (SFU) is not on the newest version", + "no_matrix_2_authorization_service": "The authorization service for your media server (SFU) is out of date.", "open_elsewhere": "Opened in another tab", "open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.", "room_creation_restricted": "Failed to create call", diff --git a/package.json b/package.json index 47adc365..14193013 100644 --- a/package.json +++ b/package.json @@ -104,8 +104,8 @@ "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#4a75d2c92f1ac7476a6d398057b91c65054f1b80", - "matrix-widget-api": "^1.14.0", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", + "matrix-widget-api": "^1.16.1", "node-stdlib-browser": "^1.3.1", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/sdk/main.ts b/sdk/main.ts index c371587f..a273ed8a 100644 --- a/sdk/main.ts +++ b/sdk/main.ts @@ -57,7 +57,7 @@ import { tryMakeSticky, widget, } from "./helper"; -import { ElementWidgetActions } from "../src/widget"; +import { ElementWidgetActions, initializeWidget } from "../src/widget"; import { type Connection } from "../src/state/CallViewModel/remoteMembers/Connection"; interface MatrixRTCSdk { @@ -88,7 +88,7 @@ export async function createMatrixRTCSdk( application: string = "m.call", id: string = "", ): Promise { - logger.info("Hello"); + initializeWidget(); const client = await widget.client; logger.info("client created"); const scope = new ObservableScope(); diff --git a/src/button/ReactionToggleButton.test.tsx b/src/button/ReactionToggleButton.test.tsx index f6b7a2ea..c6010562 100644 --- a/src/button/ReactionToggleButton.test.tsx +++ b/src/button/ReactionToggleButton.test.tsx @@ -18,7 +18,8 @@ import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; import { alice, local, localRtcMember } from "../utils/test-fixtures"; import { type MockRTCSession } from "../utils/test"; import { ReactionsSenderProvider } from "../reactions/useReactionsSender"; - +import { initializeWidget } from "../widget"; +initializeWidget(); vi.mock("livekit-client/e2ee-worker?worker"); const localIdent = `${localRtcMember.userId}:${localRtcMember.deviceId}`; diff --git a/src/initializer.tsx b/src/initializer.tsx index 419b1291..2bd6f577 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -28,6 +28,7 @@ import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; import { platform } from "./Platform"; import { isFailure } from "./utils/fetch"; +import { initializeWidget } from "./widget"; // This generates a map of locale names to their URL (based on import.meta.url), which looks like this: // { @@ -115,6 +116,8 @@ export class Initializer { } public static async initBeforeReact(): Promise { + initializeWidget(); + const polyfills: Promise[] = []; if (shouldPolyfillSegmenter()) { polyfills.push(import("@formatjs/intl-segmenter/polyfill-force")); diff --git a/src/livekit/MatrixAudioRenderer.test.tsx b/src/livekit/MatrixAudioRenderer.test.tsx index 049add97..c6122b4b 100644 --- a/src/livekit/MatrixAudioRenderer.test.tsx +++ b/src/livekit/MatrixAudioRenderer.test.tsx @@ -28,7 +28,8 @@ import { mockRemoteParticipant, mockTrack, } from "../utils/test"; - +import { initializeWidget } from "../widget"; +initializeWidget(); export const TestAudioContextConstructor = vi.fn(() => testAudioContext); const MediaDevicesProvider = MediaDevicesContext.MediaDevicesContext.Provider; diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index 733346eb..5d3eff5e 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -39,7 +39,8 @@ import { localRtcMember, } from "../utils/test-fixtures"; import { MAX_PARTICIPANT_COUNT_FOR_SOUND } from "../state/CallViewModel/CallViewModel"; - +import { initializeWidget } from "../widget"; +initializeWidget(); vitest.mock("livekit-client/e2ee-worker?worker"); vitest.mock("../useAudioContext"); vitest.mock("../soundUtils"); diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index eaf35fe1..43a689e0 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -43,7 +43,8 @@ import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer"; import { MediaDevicesContext } from "../MediaDevicesContext"; import { HeaderStyle } from "../UrlParams"; - +import { initializeWidget } from "../widget"; +initializeWidget(); vi.hoisted( () => (global.ImageData = class MockImageData { diff --git a/src/room/ReactionAudioRenderer.test.tsx b/src/room/ReactionAudioRenderer.test.tsx index 988d43a6..0f847572 100644 --- a/src/room/ReactionAudioRenderer.test.tsx +++ b/src/room/ReactionAudioRenderer.test.tsx @@ -37,6 +37,8 @@ import { local, localRtcMember, } from "../utils/test-fixtures"; +import { initializeWidget } from "../widget"; +initializeWidget(); function TestComponent({ vm }: { vm: CallViewModel }): ReactNode { return ( diff --git a/src/room/ReactionsOverlay.test.tsx b/src/room/ReactionsOverlay.test.tsx index 3ca82b1e..727fbb58 100644 --- a/src/room/ReactionsOverlay.test.tsx +++ b/src/room/ReactionsOverlay.test.tsx @@ -19,6 +19,8 @@ import { bobRtcMember, } from "../utils/test-fixtures"; import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; +import { initializeWidget } from "../widget"; +initializeWidget(); vi.mock("livekit-client/e2ee-worker?worker"); diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 376d8986..c355c627 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -62,6 +62,9 @@ import { getValue } from "../../utils/observable.ts"; import { type Behavior, constant } from "../Behavior.ts"; import { withCallViewModel as withCallViewModelInMode } from "./CallViewModelTestUtils.ts"; import { MatrixRTCMode } from "../../settings/settings.ts"; +import { initializeWidget } from "../../widget.ts"; + +initializeWidget(); vi.mock("rxjs", async (importOriginal) => ({ ...(await importOriginal()), diff --git a/src/state/CallViewModel/localMember/LocalMember.test.ts b/src/state/CallViewModel/localMember/LocalMember.test.ts index 1f2ad758..b383c295 100644 --- a/src/state/CallViewModel/localMember/LocalMember.test.ts +++ b/src/state/CallViewModel/localMember/LocalMember.test.ts @@ -40,6 +40,9 @@ import { ConnectionManagerData } from "../remoteMembers/ConnectionManager"; import { ConnectionState, type Connection } from "../remoteMembers/Connection"; import { type Publisher } from "./Publisher"; import { type LocalTransportWithSFUConfig } from "./LocalTransport"; +import { initializeWidget } from "../../../widget"; + +initializeWidget(); const MATRIX_RTC_MODE = MatrixRTCMode.Legacy; const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); diff --git a/src/state/MuteStates.test.ts b/src/state/MuteStates.test.ts index d62074c3..b7a75545 100644 --- a/src/state/MuteStates.test.ts +++ b/src/state/MuteStates.test.ts @@ -20,6 +20,8 @@ import { import { constant } from "./Behavior"; import { ObservableScope } from "./ObservableScope"; import { flushPromises, mockMediaDevices } from "../utils/test"; +import { initializeWidget } from "../widget"; +initializeWidget(); const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); vi.mock("../UrlParams", () => ({ getUrlParams })); diff --git a/src/widget.test.ts b/src/widget.test.ts new file mode 100644 index 00000000..f85c56bc --- /dev/null +++ b/src/widget.test.ts @@ -0,0 +1,127 @@ +/* +Copyright 2026 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { beforeAll, describe, expect, vi, it } from "vitest"; +import { createRoomWidgetClient, EventType } from "matrix-js-sdk"; + +import { getUrlParams } from "./UrlParams"; +import { initializeWidget, widget } from "./widget"; +import { Config } from "./config/Config"; +import { ElementCallReactionEventType } from "./reactions"; + +vi.mock("matrix-js-sdk", { spy: true }); +const createRoomWidgetClientSpy = vi.mocked(createRoomWidgetClient); + +vi.mock("./config/Config", () => ({ + Config: { + init: vi.fn().mockImplementation(async () => Promise.resolve()), + }, +})); +const configInitSpy = vi.mocked(Config.init); + +vi.mock("./UrlParams", () => ({ + getUrlParams: vi.fn(() => ({ + widgetId: "id", + parentUrl: "http://parentUrl", + roomId: "room", + userId: "myYser", + deviceId: "AAAAA", + baseUrl: "http://baseUrl", + e2eEnabled: true, + })), +})); + +initializeWidget(); +describe("widget", () => { + beforeAll(() => {}); + + it("should create an embedded client with the correct params", () => { + expect(getUrlParams()).toStrictEqual({ + widgetId: "id", + parentUrl: "http://parentUrl", + roomId: "room", + userId: "myYser", + deviceId: "AAAAA", + baseUrl: "http://baseUrl", + e2eEnabled: true, + }); + expect(widget).toBeDefined(); + expect(configInitSpy).toHaveBeenCalled(); + const sendEvent = [ + EventType.CallNotify, // Sent as a deprecated fallback + EventType.RTCNotification, + ]; + const sendRecvEvent = [ + "org.matrix.rageshake_request", + EventType.CallEncryptionKeysPrefix, + EventType.Reaction, + EventType.RoomRedaction, + ElementCallReactionEventType, + EventType.RTCDecline, + EventType.RTCMembership, + ]; + + const sendState = [ + "myYser", // Legacy call membership events + `_myYser_AAAAA_m.call`, // Session membership events + `myYser_AAAAA_m.call`, // The above with no leading underscore, for room versions whose auth rules allow it + ].map((stateKey) => ({ + eventType: EventType.GroupCallMemberPrefix, + stateKey, + })); + const receiveState = [ + { eventType: EventType.RoomCreate }, + { eventType: EventType.RoomName }, + { eventType: EventType.RoomMember }, + { eventType: EventType.RoomEncryption }, + { eventType: EventType.GroupCallMemberPrefix }, + ]; + + const sendRecvToDevice = [ + EventType.CallInvite, + EventType.CallCandidates, + EventType.CallAnswer, + EventType.CallHangup, + EventType.CallReject, + EventType.CallSelectAnswer, + EventType.CallNegotiate, + EventType.CallSDPStreamMetadataChanged, + EventType.CallSDPStreamMetadataChangedPrefix, + EventType.CallReplaces, + EventType.CallEncryptionKeysPrefix, + ]; + + expect(createRoomWidgetClientSpy.mock.calls[0][1]).toStrictEqual({ + sendEvent: [...sendEvent, ...sendRecvEvent], + receiveEvent: sendRecvEvent, + sendState, + receiveState, + sendToDevice: sendRecvToDevice, + receiveToDevice: sendRecvToDevice, + turnServers: false, + sendDelayedEvents: true, + updateDelayedEvents: true, + sendSticky: true, + receiveSticky: true, + }); + + expect(createRoomWidgetClientSpy.mock.calls[0][2]).toStrictEqual("room"); + expect(createRoomWidgetClientSpy.mock.calls[0][3]).toStrictEqual({ + baseUrl: "http://baseUrl", + userId: "myYser", + deviceId: "AAAAA", + timelineSupport: true, + useE2eForGroupCall: true, + fallbackICEServerAllowed: undefined, + store: expect.any(Object), + cryptoStore: expect.any(Object), + idBaseUrl: undefined, + scheduler: expect.any(Object), + }); + expect(createRoomWidgetClientSpy.mock.calls[0][4]).toStrictEqual(false); + }); +}); diff --git a/src/widget.ts b/src/widget.ts index 7b8201ed..16dbf514 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -6,14 +6,17 @@ Please see LICENSE in the repository root for full details. */ import { logger } from "matrix-js-sdk/lib/logger"; -import { EventType, createRoomWidgetClient } from "matrix-js-sdk"; +import { + EventType, + createRoomWidgetClient, + type MatrixClient, +} from "matrix-js-sdk"; import { WidgetApi, MatrixCapabilities, WidgetApiToWidgetAction, } from "matrix-widget-api"; -import type { MatrixClient } from "matrix-js-sdk"; import type { IWidgetApiRequest } from "matrix-widget-api"; import { LazyEventEmitter } from "./LazyEventEmitter"; import { getUrlParams } from "./UrlParams"; @@ -55,15 +58,29 @@ export interface WidgetHelpers { /** * A point of access to the widget API, if the app is running as a widget. This - * is declared and initialized on the top level because the widget messaging + * is initialized with `initializeWidget`. This should happen at the top level because the widget messaging * needs to be set up ASAP on load to ensure it doesn't miss any requests. */ -export const widget = ((): WidgetHelpers | null => { - try { - const { widgetId, parentUrl } = getUrlParams(); +export let widget: WidgetHelpers | null; + +/** + * Should be called as soon as possible on app start. (In the initilizer before react) + */ +// this needs to be a seperate call and cannot be done on import to allow us to spy on methods in here before +// execution. +export const initializeWidget = (): void => { + try { + const { + widgetId, + parentUrl, + roomId, + userId, + deviceId, + baseUrl, + e2eEnabled, + allowIceFallback, + } = getUrlParams(); - const { roomId, userId, deviceId, baseUrl, e2eEnabled, allowIceFallback } = - getUrlParams(); if (!roomId) throw new Error("Room ID must be supplied"); if (!userId) throw new Error("User ID must be supplied"); if (!deviceId) throw new Error("Device ID must be supplied"); @@ -106,6 +123,7 @@ export const widget = ((): WidgetHelpers | null => { EventType.RoomRedaction, ElementCallReactionEventType, EventType.RTCDecline, + EventType.RTCMembership, ]; const sendState = [ @@ -150,6 +168,8 @@ export const widget = ((): WidgetHelpers | null => { turnServers: false, sendDelayedEvents: true, updateDelayedEvents: true, + sendSticky: true, + receiveSticky: true, }, roomId, { @@ -172,14 +192,14 @@ export const widget = ((): WidgetHelpers | null => { return client; }; - return { api, lazyActions, client: clientPromise() }; + widget = { api, lazyActions, client: clientPromise() }; } else { if (import.meta.env.MODE !== "test") logger.info("No widget API available"); - return null; + widget = null; } } catch (e) { logger.warn("Continuing without the widget API", e); - return null; + widget = null; } -})(); +}; diff --git a/yarn.lock b/yarn.lock index 55e6a99b..8edc3d07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8364,8 +8364,8 @@ __metadata: livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "matrix-org/matrix-js-sdk#4a75d2c92f1ac7476a6d398057b91c65054f1b80" - matrix-widget-api: "npm:^1.14.0" + matrix-js-sdk: "matrix-org/matrix-js-sdk#develop" + matrix-widget-api: "npm:^1.16.1" node-stdlib-browser: "npm:^1.3.1" normalize.css: "npm:^8.0.1" observable-hooks: "npm:^4.2.3" @@ -11452,9 +11452,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@matrix-org/matrix-js-sdk#4a75d2c92f1ac7476a6d398057b91c65054f1b80": +"matrix-js-sdk@matrix-org/matrix-js-sdk#develop": version: 40.0.0 - resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=4a75d2c92f1ac7476a6d398057b91c65054f1b80" + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=dbb2ae5c0752c28639502e93f26cb3003d0d0595" dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^17.0.0" @@ -11464,23 +11464,23 @@ __metadata: jwt-decode: "npm:^4.0.0" loglevel: "npm:^1.9.2" matrix-events-sdk: "npm:0.0.1" - matrix-widget-api: "npm:^1.14.0" + matrix-widget-api: "npm:^1.16.1" oidc-client-ts: "npm:^3.0.1" p-retry: "npm:7" sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" - checksum: 10c0/4b0d970fcdd6a43a369c08ce224a7d52d30706020b30af6896e07f2335df4616c068147ae76fca46506a1c6dedadfd633d96a4c9f43ce751e2b411d2c80211d3 + checksum: 10c0/9f97cec346e0dcce8599bc3afa1608f5166408260937f8311fa9af95b8fd2ff6d86422124fcb721fc830a3ec269389067334c344b4f512b64299561484135326 languageName: node linkType: hard -"matrix-widget-api@npm:^1.14.0": - version: 1.15.0 - resolution: "matrix-widget-api@npm:1.15.0" +"matrix-widget-api@npm:^1.16.1": + version: 1.16.1 + resolution: "matrix-widget-api@npm:1.16.1" dependencies: "@types/events": "npm:^3.0.0" events: "npm:^3.2.0" - checksum: 10c0/1c08b5284cd98aed312d95594335e1391d937dfad70ef862a1f90fdbaaa27709e1c44dcda37f8045e4814779d8d5816d240aee396d52cfd9b37fbf243a6baf6a + checksum: 10c0/d88180f514104b84d3018055fc955138d65195465480a51e9afe5dbf2f3175b54e3483b4c4f1feab2dd27440f403051d9c8b293bd0532c09b136c6b23606e1ee languageName: node linkType: hard