diff --git a/package.json b/package.json index debc7ea..5b69555 100644 --- a/package.json +++ b/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 7000792..8ba26b4 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -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 = ({ children }) => { const init = async (): Promise< Pick > => { - 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 { diff --git a/src/LazyEventEmitter.ts b/src/LazyEventEmitter.ts new file mode 100644 index 0000000..bbe68c7 --- /dev/null +++ b/src/LazyEventEmitter.ts @@ -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[]]; + +/** + * 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>(); + + // 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; + } +} diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 4c25d17..3d93a33 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -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 { - // 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 diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 78e3c74..4098d0d 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -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) => { + 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) => { + 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 ; @@ -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 ; + } else if (preload) { + return null; + } else if (isEmbedded) { + return ( + +

Loading room...

+
+ ); } else { - if (isEmbedded) { - return ( - -

Loading room...

-
- ); - } else { - return ( - - ); - } + return ( + + ); } } diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index e78636a..647136e 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -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; + hideHeader: boolean; } export interface Participant { @@ -105,6 +108,7 @@ export function InCallView({ localScreenshareFeed, roomIdOrAlias, unencryptedEventsFromUsers, + hideHeader, }: Props) { usePreventScroll(); const elementRef = useRef(); @@ -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) => { + setLayout("freedom"); + await widget.api.transport.reply(ev.detail, {}); + }; + const onSpotlightLayout = async (ev: CustomEvent) => { + 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 && (
diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 9075aac..72a0c04 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -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 (
-
- - - - - - -
+ {!hideHeader && ( +
+ + + + + + +
+ )}
{groupCall.isPtt ? ( diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index 5f669b1..eed2277 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -97,6 +97,7 @@ interface Props { userMediaFeeds: CallFeed[]; onLeave: () => void; isEmbedded: boolean; + hideHeader: boolean; } export const PTTCallView: React.FC = ({ @@ -109,6 +110,7 @@ export const PTTCallView: React.FC = ({ userMediaFeeds, onLeave, isEmbedded, + hideHeader, }) => { const { modalState: inviteModalState, modalProps: inviteModalProps } = useModalTriggerState(); @@ -176,7 +178,7 @@ export const PTTCallView: React.FC = ({ // https://github.com/vector-im/element-call/issues/328 show={false} /> - {showControls && ( + {!hideHeader && showControls && (
{ 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) => ( + + ), + [client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader] + ); + if (loading || isRegistering) { return ; } @@ -73,15 +97,7 @@ export const RoomPage: FC = () => { viaServers={viaServers} createPtt={isPtt} > - {(groupCall) => ( - - )} + {groupCallView} ); diff --git a/src/room/useRoomParams.ts b/src/room/useRoomParams.ts index 070137d..095bf5f 100644 --- a/src/room/useRoomParams.ts +++ b/src/room/useRoomParams.ts @@ -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"), diff --git a/src/widget.ts b/src/widget.ts new file mode 100644 index 0000000..b6e7b98 --- /dev/null +++ b/src/widget.ts @@ -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; +} + +/** + * 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) => { + 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; + } +})(); diff --git a/yarn.lock b/yarn.lock index c1c098f..6de5bfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"