Files
element-call/src/widget.ts
T

194 lines
6.4 KiB
TypeScript
Raw Normal View History

/*
Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
2025-03-13 13:58:43 +01:00
import { logger } from "matrix-js-sdk/lib/logger";
2026-01-22 19:23:08 +01:00
import {
EventType,
createRoomWidgetClient,
type MatrixClient,
} from "matrix-js-sdk";
2024-12-17 19:42:04 -05:00
import {
WidgetApi,
MatrixCapabilities,
WidgetApiToWidgetAction,
2025-03-13 18:15:58 +01:00
} from "matrix-widget-api";
2025-03-13 18:15:58 +01:00
import type { IWidgetApiRequest } from "matrix-widget-api";
import { LazyEventEmitter } from "./LazyEventEmitter";
2022-10-10 09:19:10 -04:00
import { getUrlParams } from "./UrlParams";
2023-07-10 13:36:07 +01:00
import { Config } from "./config/Config";
2024-11-08 17:36:40 +00:00
import { ElementCallReactionEventType } from "./reactions";
// Subset of the actions in element-web
export enum ElementWidgetActions {
JoinCall = "io.element.join",
HangupCall = "im.vector.hangup",
Close = "io.element.close",
// This can be sent as from or to widget
// fromWidget: updates the client about the current device mute state
// toWidget: the client requests a specific device mute configuration
// The reply will always be the resulting configuration
// It is possible to sent an empty configuration to retrieve the current values or
// just one of the fields to update that particular value
// An undefined field means that EC will keep the mute state as is.
// -> this will allow the client to only get the current state
//
// The data of the widget action request and the response are:
// {
// audio_enabled?: boolean,
// video_enabled?: boolean
// }
DeviceMute = "io.element.device_mute",
}
export interface JoinCallData {
audioInput: string | null;
videoInput: string | null;
}
export interface WidgetHelpers {
api: WidgetApi;
lazyActions: LazyEventEmitter;
client: Promise<MatrixClient>;
}
/**
* A point of access to the widget API, if the app is running as a widget. This
2026-01-22 19:23:08 +01:00
* 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.
*/
2026-01-22 19:23:08 +01:00
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.
2026-01-25 14:24:31 +01:00
export const initializeWidget = (rtcApplication: string = "m.call"): void => {
try {
2026-01-22 19:23:08 +01:00
const {
widgetId,
parentUrl,
roomId,
userId,
deviceId,
baseUrl,
e2eEnabled,
allowIceFallback,
} = getUrlParams();
2025-11-30 20:31:21 +01:00
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");
if (!baseUrl) throw new Error("Base URL must be supplied");
if (widgetId && parentUrl) {
const parentOrigin = new URL(parentUrl).origin;
logger.info("Widget API is available");
const api = new WidgetApi(widgetId, parentOrigin);
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
// Set up the lazy action emitter, but only for select actions that we
// intend for the app to handle
const lazyActions = new LazyEventEmitter();
[
2024-12-17 19:42:04 -05:00
WidgetApiToWidgetAction.ThemeChange,
ElementWidgetActions.JoinCall,
ElementWidgetActions.HangupCall,
ElementWidgetActions.DeviceMute,
].forEach((action) => {
api.on(`action:${action}`, (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
lazyActions.emit(action, ev);
});
});
// Now, initialize the matryoshka MatrixClient (so named because it routes
// all requests through the host client via the widget API)
// We need to do this now rather than later because it has capabilities to
// request, and is responsible for starting the transport (should it be?)
// These are all the event types the app uses
const sendEvent = [
EventType.CallNotify, // Sent as a deprecated fallback
EventType.RTCNotification,
];
const sendRecvEvent = [
"org.matrix.rageshake_request",
EventType.CallEncryptionKeysPrefix,
2024-11-04 08:54:13 -01:00
EventType.Reaction,
EventType.RoomRedaction,
2024-11-08 17:36:40 +00:00
ElementCallReactionEventType,
EventType.RTCDecline,
EventType.RTCMembership,
];
const sendState = [
2024-07-25 14:33:37 +02:00
userId, // Legacy call membership events
2026-01-25 14:24:31 +01:00
`_${userId}_${deviceId}_${rtcApplication}`, // Session membership events
`${userId}_${deviceId}_${rtcApplication}`, // 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 },
2025-06-26 05:08:57 -04:00
{ eventType: EventType.RoomName },
{ eventType: EventType.RoomMember },
2024-04-23 15:15:13 +02:00
{ eventType: EventType.RoomEncryption },
{ eventType: EventType.GroupCallMemberPrefix },
];
2026-01-25 14:24:31 +01:00
const sendRecvToDevice = [EventType.CallEncryptionKeysPrefix];
const client = createRoomWidgetClient(
api,
{
sendEvent: [...sendEvent, ...sendRecvEvent],
receiveEvent: sendRecvEvent,
sendState,
receiveState,
sendToDevice: sendRecvToDevice,
receiveToDevice: sendRecvToDevice,
2023-09-12 13:51:06 +02:00
turnServers: false,
sendDelayedEvents: true,
updateDelayedEvents: true,
sendSticky: true,
receiveSticky: true,
},
roomId,
{
2022-10-17 01:46:44 -04:00
baseUrl,
userId,
deviceId,
timelineSupport: true,
2023-03-01 14:30:25 +01:00
useE2eForGroupCall: e2eEnabled,
fallbackICEServerAllowed: allowIceFallback,
2023-10-11 10:42:04 -04:00
},
2024-03-13 14:58:21 +01:00
// ContentLoaded event will be sent as soon as the theme is set (see useTheme.ts)
false,
);
2023-07-10 13:36:07 +01:00
const clientPromise = async (): Promise<MatrixClient> => {
// Wait for the config file to be ready (we load very early on so it might not
// be otherwise)
await Config.init();
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 });
return client;
};
2026-01-22 19:23:08 +01:00
widget = { api, lazyActions, client: clientPromise() };
} else {
if (import.meta.env.MODE !== "test")
logger.info("No widget API available");
2026-01-22 19:23:08 +01:00
widget = null;
}
} catch (e) {
logger.warn("Continuing without the widget API", e);
2026-01-22 19:23:08 +01:00
widget = null;
}
2026-01-22 19:23:08 +01:00
};