Compare commits

..

7 Commits

Author SHA1 Message Date
Timo K
536a5f60dc small code comment fixups 2026-02-04 19:55:49 +01:00
Timo
15c39372f4 Merge pull request #3697 from element-hq/renovate/npm-lodash-es-vulnerability
Update dependency lodash-es to v4.17.23 [SECURITY]
2026-01-23 10:51:41 +01:00
Timo
330ad3448d Merge pull request #3686 from element-hq/robin/sticky-widget
Make Element Call widgets compatible with Matrix 2.0 mode
2026-01-22 20:17:16 +01:00
Timo K
220fc51eb5 add initializeWidget to tests. 2026-01-22 19:38:39 +01:00
Timo K
b754106feb Add widget.ts tests 2026-01-22 19:23:08 +01:00
renovate[bot]
05107a3e5c Update dependency lodash-es to v4.17.23 [SECURITY] 2026-01-22 04:37:37 +00:00
Robin
d516f0f602 Make Element Call widgets compatible with Matrix 2.0 mode
This gives Element Call widgets the ability to send and receive sticky RTC membership events.
2026-01-21 19:59:34 +01:00
18 changed files with 204 additions and 38 deletions

View File

@@ -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",

View File

@@ -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#5000a4760723c95b47496fbd3841ab81538a32cf",
"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",

View File

@@ -21,7 +21,7 @@ in the repository root.
It will create a `dist` folder containing the compiled js file.
This file needs to be hosted. Locally (via `npx serve -l 81234 --cors`) or on a remote server.
This file needs to be hosted. Locally (via `npx serve -l 1234 --cors`) or on a remote server.
Now you just need to add the widget to element web via:

View File

@@ -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<MatrixRTCSdk> {
logger.info("Hello");
initializeWidget();
const client = await widget.client;
logger.info("client created");
const scope = new ObservableScope();

View File

@@ -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}`;

View File

@@ -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<void> {
initializeWidget();
const polyfills: Promise<unknown>[] = [];
if (shouldPolyfillSegmenter()) {
polyfills.push(import("@formatjs/intl-segmenter/polyfill-force"));

View File

@@ -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;

View File

@@ -78,8 +78,8 @@ export type OpenIDClientParts = Pick<
* @param membership Our own membership identity parts used to send to jwt service.
* @param serviceUrl The URL of the livekit SFU service
* @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases.
* @param opts Additional options to modify which endpoint with which data will be used to aquire the jwt token.
* @param opts.forceJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination
* @param opts Additional options to modify which endpoint with which data will be used to acquire the jwt token.
* @param opts.forceJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatenation
* instead of a hash.
* This function by default uses whatever is possible with the current jwt service installed next to the SFU.
* For remote connections this does not matter, since we will not publish there we can rely on the newest option.
@@ -146,7 +146,7 @@ export async function getSFUConfigWithOpenID(
} else {
logger?.warn(
`Failed fetching jwt with matrix 2.0 endpoint other issues ->`,
`(not going to try with legacy endpoint: forceOldJwtEndpoint is set to false, we did not get a not supported error from the sfu)`,
`(not going to try with legacy endpoint if forceMatrix2Jwt is set to false (it is ${forceMatrix2Jwt}), we did not get a not supported error from the sfu)`,
e,
);
// Make this throw a hard error in case we force the matrix2.0 endpoint.

View File

@@ -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");

View File

@@ -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 {

View File

@@ -37,6 +37,8 @@ import {
local,
localRtcMember,
} from "../utils/test-fixtures";
import { initializeWidget } from "../widget";
initializeWidget();
function TestComponent({ vm }: { vm: CallViewModel }): ReactNode {
return (

View File

@@ -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");

View File

@@ -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()),

View File

@@ -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(() => ({})));

View File

@@ -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 }));

127
src/widget.test.ts Normal file
View File

@@ -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);
});
});

View File

@@ -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;
}
})();
};

View File

@@ -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#5000a4760723c95b47496fbd3841ab81538a32cf"
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"
@@ -11262,9 +11262,9 @@ __metadata:
linkType: hard
"lodash-es@npm:^4.17.21":
version: 4.17.22
resolution: "lodash-es@npm:4.17.22"
checksum: 10c0/5f28a262183cca43e08c580622557f393cb889386df2d8adf7c852bfdff7a84c5e629df5aa6c5c6274e83b38172f239d3e4e72e1ad27352d9ae9766627338089
version: 4.17.23
resolution: "lodash-es@npm:4.17.23"
checksum: 10c0/3150fb6660c14c7a6b5f23bd11597d884b140c0e862a17fdb415aaa5ef7741523182904a6b7929f04e5f60a11edb5a79499eb448734381c99ffb3c4734beeddd
languageName: node
linkType: hard
@@ -11452,9 +11452,9 @@ __metadata:
languageName: node
linkType: hard
"matrix-js-sdk@matrix-org/matrix-js-sdk#5000a4760723c95b47496fbd3841ab81538a32cf":
"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=5000a4760723c95b47496fbd3841ab81538a32cf"
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/3dadc5f34bc6a12321fd7d5b0c93334ba92f9c5e496ae23980f03afbd9a9686e30a34e8583c6ae8291cedf636fe2d29d71849137e58b08e311654260589fc154
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