mirror of
https://github.com/vector-im/element-call.git
synced 2026-01-30 03:15:55 +00:00
Merge pull request #3686 from element-hq/robin/sticky-widget
Make Element Call widgets compatible with Matrix 2.0 mode
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
local,
|
||||
localRtcMember,
|
||||
} from "../utils/test-fixtures";
|
||||
import { initializeWidget } from "../widget";
|
||||
initializeWidget();
|
||||
|
||||
function TestComponent({ vm }: { vm: CallViewModel }): ReactNode {
|
||||
return (
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(() => ({})));
|
||||
|
||||
@@ -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
127
src/widget.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
20
yarn.lock
20
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user