Merge pull request #574 from robintown/ec-in-ew

Prepare for integration into Element Web
This commit is contained in:
Robin 2022-09-09 10:01:40 -04:00 committed by GitHub
commit 3bffe58549
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 434 additions and 144 deletions

View file

@ -38,7 +38,7 @@
"classnames": "^2.3.1",
"color-hash": "^2.0.1",
"events": "^3.3.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#83c848093fe49652aedee71d963dfe07fd6d73f2",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#36a6117ee284cefe7d16055352c9cefce30ce6b1",
"matrix-widget-api": "^1.0.0",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",
@ -79,4 +79,4 @@
"vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^0.4.0"
}
}
}

View file

@ -31,10 +31,10 @@ import { logger } from "matrix-js-sdk/src/logger";
import { ErrorView } from "./FullScreenView";
import {
initClient,
initMatroskaClient,
defaultHomeserver,
CryptoStoreIntegrityError,
} from "./matrix-utils";
import { widget } from "./widget";
declare global {
interface Window {
@ -100,16 +100,12 @@ export const ClientProvider: FC<Props> = ({ children }) => {
const init = async (): Promise<
Pick<ClientProviderState, "client" | "isPasswordlessUser">
> => {
const query = new URLSearchParams(window.location.search);
const widgetId = query.get("widgetId");
const parentUrl = query.get("parentUrl");
if (widgetId && parentUrl) {
// We're inside a widget, so let's engage *Matroska mode*
logger.log("Using a Matroska client");
if (widget) {
// We're inside a widget, so let's engage *matryoshka mode*
logger.log("Using a matryoshka client");
return {
client: await initMatroskaClient(widgetId, parentUrl),
client: await widget.client,
isPasswordlessUser: false,
};
} else {

90
src/LazyEventEmitter.ts Normal file
View file

@ -0,0 +1,90 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from "events";
type NonEmptyArray<T> = [T, ...T[]];
/**
* An event emitter that lets events pile up in a backlog until a listener is
* present, at which point any events that were missed are re-emitted.
*/
export class LazyEventEmitter extends EventEmitter {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private eventBacklogs = new Map<string | symbol, NonEmptyArray<any[]>>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public emit(type: string | symbol, ...args: any[]): boolean {
const hasListeners = super.emit(type, ...args);
if (!hasListeners) {
// The event was missed, so add it to the backlog
const backlog = this.eventBacklogs.get(type);
if (backlog) {
backlog.push(args);
} else {
// Start a new backlog
this.eventBacklogs.set(type, [args]);
}
}
return hasListeners;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public on(type: string | symbol, listener: (...args: any[]) => void): this {
super.on(type, listener);
const backlog = this.eventBacklogs.get(type);
if (backlog) {
// That was the first listener for this type, so let's send it all the
// events that have piled up
for (const args of backlog) super.emit(type, ...args);
// Backlog is now clear
this.eventBacklogs.delete(type);
}
return this;
}
public addListener(
type: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (...args: any[]) => void
): this {
return this.on(type, listener);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public once(type: string | symbol, listener: (...args: any[]) => void): this {
super.once(type, listener);
const backlog = this.eventBacklogs.get(type);
if (backlog) {
// That was the first listener for this type, so let's send it the first
// of the events that have piled up
super.emit(type, ...backlog[0]);
// Clear the event from the backlog
if (backlog.length === 1) {
this.eventBacklogs.delete(type);
} else {
backlog.shift();
}
}
return this;
}
}

View file

@ -5,23 +5,18 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory";
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
import {
createClient,
createRoomWidgetClient,
MatrixClient,
} from "matrix-js-sdk/src/matrix";
import { createClient } from "matrix-js-sdk/src/matrix";
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
import { ClientEvent } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { WidgetApi } from "matrix-widget-api";
import { logger } from "matrix-js-sdk/src/logger";
import {
GroupCallIntent,
GroupCallType,
} from "matrix-js-sdk/src/webrtc/groupCall";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room } from "matrix-js-sdk/src/models/room";
import IndexedDBWorker from "./IndexedDBWorker?worker";
import { getRoomParams } from "./room/useRoomParams";
@ -64,73 +59,6 @@ function waitForSync(client: MatrixClient) {
});
}
/**
* Initialises and returns a new widget-API-based Matrix Client.
* @param widgetId The ID of the widget that the app is running inside.
* @param parentUrl The URL of the parent client.
* @returns The MatrixClient instance
*/
export async function initMatroskaClient(
widgetId: string,
parentUrl: string
): Promise<MatrixClient> {
// In this mode, we use a special client which routes all requests through
// the host application via the widget API
const { roomId, userId, deviceId } = getRoomParams();
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");
// These are all the event types the app uses
const sendState = [
{ eventType: EventType.GroupCallPrefix },
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
];
const receiveState = [
{ eventType: EventType.RoomMember },
{ eventType: EventType.GroupCallPrefix },
{ 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,
"org.matrix.call_duplicate_session",
];
// Since all data should be coming from the host application, there's no
// need to persist anything, and therefore we can use the default stores
// We don't even need to set up crypto
const client = createRoomWidgetClient(
new WidgetApi(widgetId, new URL(parentUrl).origin),
{
sendState,
receiveState,
sendToDevice: sendRecvToDevice,
receiveToDevice: sendRecvToDevice,
turnServers: true,
},
roomId,
{
baseUrl: "",
userId,
deviceId,
timelineSupport: true,
}
);
await client.startClient();
return client;
}
/**
* Initialises and returns a new standalone Matrix Client.
* If true is passed for the 'restore' parameter, a check will be made

View file

@ -19,6 +19,8 @@ import { useHistory } from "react-router-dom";
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView";
@ -28,22 +30,30 @@ import { CallEndedView } from "./CallEndedView";
import { useRoomAvatar } from "./useRoomAvatar";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { useLocationNavigation } from "../useLocationNavigation";
import { useMediaHandler } from "../settings/useMediaHandler";
declare global {
interface Window {
groupCall: GroupCall;
groupCall?: GroupCall;
}
}
interface Props {
client: MatrixClient;
isPasswordlessUser: boolean;
isEmbedded: boolean;
preload: boolean;
hideHeader: boolean;
roomIdOrAlias: string;
groupCall: GroupCall;
}
export function GroupCallView({
client,
isPasswordlessUser,
isEmbedded,
preload,
hideHeader,
roomIdOrAlias,
groupCall,
}: Props) {
@ -69,14 +79,50 @@ export function GroupCallView({
unencryptedEventsFromUsers,
} = useGroupCall(groupCall);
const { setAudioInput, setVideoInput } = useMediaHandler();
const avatarUrl = useRoomAvatar(groupCall.room);
useEffect(() => {
window.groupCall = groupCall;
return () => {
delete window.groupCall;
};
}, [groupCall]);
// In embedded mode, bypass the lobby and just enter the call straight away
if (isEmbedded) groupCall.enter();
}, [groupCall, isEmbedded]);
useEffect(() => {
if (widget && preload) {
// In preload mode, wait for a join action before entering
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => {
const { audioInput, videoInput } = ev.detail
.data as unknown as JoinCallData;
if (audioInput !== null) setAudioInput(audioInput);
if (videoInput !== null) setVideoInput(videoInput);
await Promise.all([
groupCall.setMicrophoneMuted(audioInput === null),
groupCall.setLocalVideoMuted(videoInput === null),
]);
await groupCall.enter();
await Promise.all([
widget.api.setAlwaysOnScreen(true),
widget.api.transport.reply(ev.detail, {}),
]);
};
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
return () => {
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
};
}
}, [groupCall, preload, setAudioInput, setVideoInput]);
useEffect(() => {
if (isEmbedded && !preload) {
// In embedded mode, bypass the lobby and just enter the call straight away
groupCall.enter();
}
}, [groupCall, isEmbedded, preload]);
useSentryGroupCallHandler(groupCall);
@ -86,13 +132,31 @@ export function GroupCallView({
const history = useHistory();
const onLeave = useCallback(() => {
setLeft(true);
leave();
if (widget) {
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
widget.api.setAlwaysOnScreen(false);
}
if (!isPasswordlessUser) {
if (isPasswordlessUser) {
setLeft(true);
} else if (!isEmbedded) {
history.push("/");
}
}, [leave, isPasswordlessUser, history]);
}, [leave, isPasswordlessUser, isEmbedded, history]);
useEffect(() => {
if (widget && state === GroupCallState.Entered) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
leave();
await widget.api.transport.reply(ev.detail, {});
};
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
return () => {
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
};
}
}, [groupCall, state, leave]);
if (error) {
return <ErrorView error={error} />;
@ -109,6 +173,7 @@ export function GroupCallView({
userMediaFeeds={userMediaFeeds}
onLeave={onLeave}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
/>
);
} else {
@ -131,6 +196,7 @@ export function GroupCallView({
screenshareFeeds={screenshareFeeds}
roomIdOrAlias={roomIdOrAlias}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
/>
);
}
@ -142,32 +208,33 @@ export function GroupCallView({
);
} else if (left) {
return <CallEndedView client={client} />;
} else if (preload) {
return null;
} else if (isEmbedded) {
return (
<FullScreenView>
<h1>Loading room...</h1>
</FullScreenView>
);
} else {
if (isEmbedded) {
return (
<FullScreenView>
<h1>Loading room...</h1>
</FullScreenView>
);
} else {
return (
<LobbyView
client={client}
groupCall={groupCall}
roomName={groupCall.room.name}
avatarUrl={avatarUrl}
state={state}
onInitLocalCallFeed={initLocalCallFeed}
localCallFeed={localCallFeed}
onEnter={enter}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
toggleMicrophoneMuted={toggleMicrophoneMuted}
roomIdOrAlias={roomIdOrAlias}
isEmbedded={isEmbedded}
/>
);
}
return (
<LobbyView
client={client}
groupCall={groupCall}
roomName={groupCall.room.name}
avatarUrl={avatarUrl}
state={state}
onInitLocalCallFeed={initLocalCallFeed}
localCallFeed={localCallFeed}
onEnter={enter}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
toggleMicrophoneMuted={toggleMicrophoneMuted}
roomIdOrAlias={roomIdOrAlias}
isEmbedded={isEmbedded}
hideHeader={hideHeader}
/>
);
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useMemo, useRef } from "react";
import React, { useEffect, useCallback, useMemo, useRef } from "react";
import { usePreventScroll } from "@react-aria/overlays";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -22,6 +22,7 @@ import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import classNames from "classnames";
import type { IWidgetApiRequest } from "matrix-widget-api";
import styles from "./InCallView.module.css";
import {
HangupButton,
@ -52,6 +53,7 @@ import { useAudioContext } from "../video-grid/useMediaStream";
import { useFullscreen } from "../video-grid/useFullscreen";
import { AudioContainer } from "../video-grid/AudioContainer";
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
import { widget, ElementWidgetActions } from "../widget";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@ -77,6 +79,7 @@ interface Props {
localScreenshareFeed: CallFeed;
roomIdOrAlias: string;
unencryptedEventsFromUsers: Set<string>;
hideHeader: boolean;
}
export interface Participant {
@ -105,6 +108,7 @@ export function InCallView({
localScreenshareFeed,
roomIdOrAlias,
unencryptedEventsFromUsers,
hideHeader,
}: Props) {
usePreventScroll();
const elementRef = useRef<HTMLDivElement>();
@ -122,6 +126,42 @@ export function InCallView({
useAudioOutputDevice(audioRef, audioOutput);
useEffect(() => {
widget?.api.transport.send(
layout === "freedom"
? ElementWidgetActions.TileLayout
: ElementWidgetActions.SpotlightLayout,
{}
);
}, [layout]);
useEffect(() => {
if (widget) {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("freedom");
await widget.api.transport.reply(ev.detail, {});
};
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("spotlight");
await widget.api.transport.reply(ev.detail, {});
};
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
widget.lazyActions.on(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout
);
return () => {
widget.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
widget.lazyActions.off(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout
);
};
}
}, [setLayout]);
const items = useMemo(() => {
const participants: Participant[] = [];
@ -246,7 +286,7 @@ export function InCallView({
audioDestination={audioDestination}
/>
)}
{!fullscreenParticipant && (
{!hideHeader && !fullscreenParticipant && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />

View file

@ -47,6 +47,7 @@ interface Props {
localVideoMuted: boolean;
roomIdOrAlias: string;
isEmbedded: boolean;
hideHeader: boolean;
}
export function LobbyView({
client,
@ -63,6 +64,7 @@ export function LobbyView({
toggleMicrophoneMuted,
roomIdOrAlias,
isEmbedded,
hideHeader,
}: Props) {
const { stream } = useCallFeed(localCallFeed);
const {
@ -90,14 +92,16 @@ export function LobbyView({
return (
<div className={styles.room}>
<Header>
<LeftNav>
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
</LeftNav>
<RightNav>
<UserMenuContainer />
</RightNav>
</Header>
{!hideHeader && (
<Header>
<LeftNav>
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
</LeftNav>
<RightNav>
<UserMenuContainer />
</RightNav>
</Header>
)}
<div className={styles.joinRoom}>
<div className={styles.joinRoomContent}>
{groupCall.isPtt ? (

View file

@ -97,6 +97,7 @@ interface Props {
userMediaFeeds: CallFeed[];
onLeave: () => void;
isEmbedded: boolean;
hideHeader: boolean;
}
export const PTTCallView: React.FC<Props> = ({
@ -109,6 +110,7 @@ export const PTTCallView: React.FC<Props> = ({
userMediaFeeds,
onLeave,
isEmbedded,
hideHeader,
}) => {
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
@ -176,7 +178,7 @@ export const PTTCallView: React.FC<Props> = ({
// https://github.com/vector-im/element-call/issues/328
show={false}
/>
{showControls && (
{!hideHeader && showControls && (
<Header className={styles.header}>
<LeftNav>
<RoomSetupHeaderInfo

View file

@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FC, useEffect, useState } from "react";
import React, { FC, useEffect, useState, useCallback } from "react";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
@ -29,8 +30,16 @@ export const RoomPage: FC = () => {
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
const { roomAlias, roomId, viaServers, isEmbedded, isPtt, displayName } =
useRoomParams();
const {
roomAlias,
roomId,
viaServers,
isEmbedded,
preload,
hideHeader,
isPtt,
displayName,
} = useRoomParams();
const roomIdOrAlias = roomId ?? roomAlias;
if (!roomIdOrAlias) throw new Error("No room specified");
@ -53,6 +62,21 @@ export const RoomPage: FC = () => {
registerPasswordlessUser,
]);
const groupCallView = useCallback(
(groupCall: GroupCall) => (
<GroupCallView
client={client}
roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall}
isPasswordlessUser={isPasswordlessUser}
isEmbedded={isEmbedded}
preload={preload}
hideHeader={hideHeader}
/>
),
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader]
);
if (loading || isRegistering) {
return <LoadingView />;
}
@ -73,15 +97,7 @@ export const RoomPage: FC = () => {
viaServers={viaServers}
createPtt={isPtt}
>
{(groupCall) => (
<GroupCallView
client={client}
roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall}
isPasswordlessUser={isPasswordlessUser}
isEmbedded={isEmbedded}
/>
)}
{groupCallView}
</GroupCallLoader>
</MediaHandlerProvider>
);

View file

@ -24,6 +24,11 @@ export interface RoomParams {
// Whether the app is running in embedded mode, and should keep the user
// confined to the current room
isEmbedded: boolean;
// Whether the app should pause before joining the call until it sees an
// io.element.join widget action, allowing it to be preloaded
preload: boolean;
// Whether to hide the room header when in a call
hideHeader: boolean;
// Whether to start a walkie-talkie call instead of a video call
isPtt: boolean;
// Whether to use end-to-end encryption
@ -75,6 +80,8 @@ export const getRoomParams = (
roomId: getParam("roomId"),
viaServers: getAllParams("via"),
isEmbedded: hasParam("embed"),
preload: hasParam("preload"),
hideHeader: hasParam("hideHeader"),
isPtt: hasParam("ptt"),
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
userId: getParam("userId"),

140
src/widget.ts Normal file
View file

@ -0,0 +1,140 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { createRoomWidgetClient } from "matrix-js-sdk/src/matrix";
import { WidgetApi, MatrixCapabilities } from "matrix-widget-api";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { LazyEventEmitter } from "./LazyEventEmitter";
import { getRoomParams } from "./room/useRoomParams";
// Subset of the actions in matrix-react-sdk
export enum ElementWidgetActions {
JoinCall = "io.element.join",
HangupCall = "im.vector.hangup",
TileLayout = "io.element.tile_layout",
SpotlightLayout = "io.element.spotlight_layout",
}
export interface JoinCallData {
audioInput: string | null;
videoInput: string | null;
}
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
* is declared and initialized on 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 query = new URLSearchParams(window.location.search);
const widgetId = query.get("widgetId");
const parentUrl = query.get("parentUrl");
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();
[
ElementWidgetActions.JoinCall,
ElementWidgetActions.HangupCall,
ElementWidgetActions.TileLayout,
ElementWidgetActions.SpotlightLayout,
].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?)
const { roomId, userId, deviceId } = getRoomParams();
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");
// These are all the event types the app uses
const sendState = [
{ eventType: EventType.GroupCallPrefix },
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
];
const receiveState = [
{ eventType: EventType.RoomMember },
{ eventType: EventType.GroupCallPrefix },
{ 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,
"org.matrix.call_duplicate_session",
];
const client = createRoomWidgetClient(
api,
{
sendState,
receiveState,
sendToDevice: sendRecvToDevice,
receiveToDevice: sendRecvToDevice,
turnServers: true,
},
roomId,
{
baseUrl: "",
userId,
deviceId,
timelineSupport: true,
}
);
const clientPromise = client.startClient().then(() => client);
return { api, lazyActions, client: clientPromise };
} else {
logger.info("No widget API available");
return null;
}
} catch (e) {
logger.warn("Continuing without the widget API", e);
return null;
}
})();

View file

@ -8390,9 +8390,9 @@ matrix-events-sdk@^0.0.1-beta.7:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#83c848093fe49652aedee71d963dfe07fd6d73f2":
version "19.3.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/83c848093fe49652aedee71d963dfe07fd6d73f2"
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#36a6117ee284cefe7d16055352c9cefce30ce6b1":
version "19.4.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/36a6117ee284cefe7d16055352c9cefce30ce6b1"
dependencies:
"@babel/runtime" "^7.12.5"
"@types/sdp-transform" "^2.4.5"