From 3186b5f24baa10f0b7bd9482a5c2222a59945cac Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 9 Sep 2022 02:04:53 -0400 Subject: [PATCH 1/4] Add a URL parameter for hiding the room header --- src/room/GroupCallView.tsx | 9 ++++++++- src/room/InCallView.tsx | 4 +++- src/room/LobbyView.tsx | 20 ++++++++++++-------- src/room/PTTCallView.tsx | 4 +++- src/room/RoomPage.tsx | 35 ++++++++++++++++++++++++----------- src/room/useRoomParams.ts | 3 +++ 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 78e3c74..cf0b401 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -30,20 +30,24 @@ import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { useLocationNavigation } from "../useLocationNavigation"; declare global { interface Window { - groupCall: GroupCall; + groupCall?: GroupCall; } } + interface Props { client: MatrixClient; isPasswordlessUser: boolean; isEmbedded: boolean; + hideHeader: boolean; roomIdOrAlias: string; groupCall: GroupCall; } + export function GroupCallView({ client, isPasswordlessUser, isEmbedded, + hideHeader, roomIdOrAlias, groupCall, }: Props) { @@ -109,6 +113,7 @@ export function GroupCallView({ userMediaFeeds={userMediaFeeds} onLeave={onLeave} isEmbedded={isEmbedded} + hideHeader={hideHeader} /> ); } else { @@ -131,6 +136,7 @@ export function GroupCallView({ screenshareFeeds={screenshareFeeds} roomIdOrAlias={roomIdOrAlias} unencryptedEventsFromUsers={unencryptedEventsFromUsers} + hideHeader={hideHeader} /> ); } @@ -166,6 +172,7 @@ export function GroupCallView({ toggleMicrophoneMuted={toggleMicrophoneMuted} roomIdOrAlias={roomIdOrAlias} isEmbedded={isEmbedded} + hideHeader={hideHeader} /> ); } diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index e78636a..347cb5d 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -77,6 +77,7 @@ interface Props { localScreenshareFeed: CallFeed; roomIdOrAlias: string; unencryptedEventsFromUsers: Set; + hideHeader: boolean; } export interface Participant { @@ -105,6 +106,7 @@ export function InCallView({ localScreenshareFeed, roomIdOrAlias, unencryptedEventsFromUsers, + hideHeader, }: Props) { usePreventScroll(); const elementRef = useRef(); @@ -246,7 +248,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, + hideHeader, + isPtt, + displayName, + } = useRoomParams(); const roomIdOrAlias = roomId ?? roomAlias; if (!roomIdOrAlias) throw new Error("No room specified"); @@ -53,6 +60,20 @@ export const RoomPage: FC = () => { registerPasswordlessUser, ]); + const groupCallView = useCallback( + (groupCall: GroupCall) => ( + + ), + [client, roomIdOrAlias, isPasswordlessUser, isEmbedded, hideHeader] + ); + if (loading || isRegistering) { return ; } @@ -73,15 +94,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..5edfe4e 100644 --- a/src/room/useRoomParams.ts +++ b/src/room/useRoomParams.ts @@ -24,6 +24,8 @@ export interface RoomParams { // Whether the app is running in embedded mode, and should keep the user // confined to the current room isEmbedded: 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 +77,7 @@ export const getRoomParams = ( roomId: getParam("roomId"), viaServers: getAllParams("via"), isEmbedded: hasParam("embed"), + hideHeader: hasParam("hideHeader"), isPtt: hasParam("ptt"), e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true userId: getParam("userId"), From f0045c9406dd5768e8bb112813e95f10c8508437 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 9 Sep 2022 02:08:17 -0400 Subject: [PATCH 2/4] Initialize all widget-related things at the top level --- src/ClientContext.tsx | 14 ++-- src/LazyEventEmitter.ts | 90 ++++++++++++++++++++++++++ src/matrix-utils.ts | 76 +--------------------- src/widget.ts | 140 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 83 deletions(-) create mode 100644 src/LazyEventEmitter.ts create mode 100644 src/widget.ts 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/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; + } +})(); From b7be3011da65b8e4e4ced048607010419e1a5994 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 9 Sep 2022 02:10:45 -0400 Subject: [PATCH 3/4] Add widget actions for joining and leaving calls and switching layouts These actions are processed lazily to ensure that even if the app takes a while to start up, they won't be missed. --- src/room/GroupCallView.tsx | 126 +++++++++++++++++++++++++++---------- src/room/InCallView.tsx | 40 +++++++++++- src/room/RoomPage.tsx | 7 ++- src/room/useRoomParams.ts | 4 ++ 4 files changed, 141 insertions(+), 36 deletions(-) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index cf0b401..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,6 +30,8 @@ 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; @@ -38,6 +42,7 @@ interface Props { client: MatrixClient; isPasswordlessUser: boolean; isEmbedded: boolean; + preload: boolean; hideHeader: boolean; roomIdOrAlias: string; groupCall: GroupCall; @@ -47,6 +52,7 @@ export function GroupCallView({ client, isPasswordlessUser, isEmbedded, + preload, hideHeader, roomIdOrAlias, groupCall, @@ -73,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); @@ -90,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 ; @@ -148,33 +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 347cb5d..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 @@ -124,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[] = []; diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index 3c41118..1adff57 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -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"; @@ -34,6 +35,7 @@ export const RoomPage: FC = () => { roomId, viaServers, isEmbedded, + preload, hideHeader, isPtt, displayName, @@ -68,10 +70,11 @@ export const RoomPage: FC = () => { groupCall={groupCall} isPasswordlessUser={isPasswordlessUser} isEmbedded={isEmbedded} + preload={preload} hideHeader={hideHeader} /> ), - [client, roomIdOrAlias, isPasswordlessUser, isEmbedded, hideHeader] + [client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader] ); if (loading || isRegistering) { diff --git a/src/room/useRoomParams.ts b/src/room/useRoomParams.ts index 5edfe4e..095bf5f 100644 --- a/src/room/useRoomParams.ts +++ b/src/room/useRoomParams.ts @@ -24,6 +24,9 @@ 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 @@ -77,6 +80,7 @@ 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 From e8bc22370b2d7c92904ad6f633c3a0683d9f454b Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 9 Sep 2022 09:54:26 -0400 Subject: [PATCH 4/4] Upgrade matrix-js-sdk --- package.json | 4 ++-- yarn.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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/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"