diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 3845182f4f..9dee7594ea 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -84,6 +84,7 @@ import { CallError } from "matrix-js-sdk/src/webrtc/call"; import { logger } from 'matrix-js-sdk/src/logger'; import { Action } from './dispatcher/actions'; import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper'; +import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; const CHECK_PSTN_SUPPORT_ATTEMPTS = 3; @@ -581,6 +582,12 @@ export default class CallHandler { switch (payload.action) { case 'place_call': { + // We might be using managed hybrid widgets + if (isManagedHybridWidgetEnabled()) { + addManagedHybridWidget(payload.room_id); + return; + } + // if the runtime env doesn't do VoIP, whine. if (!MatrixClientPeg.get().supportsVoip()) { Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index 86ce9c97d4..e6ef534202 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -41,7 +41,7 @@ export enum Container { // are only two containers, and that only the top container is special. } -interface IStoredLayout { +export interface IStoredLayout { // Where to store the widget. Required. container: Container; diff --git a/src/utils/WellKnownUtils.ts b/src/utils/WellKnownUtils.ts index 69ed39e0ee..30759cafc1 100644 --- a/src/utils/WellKnownUtils.ts +++ b/src/utils/WellKnownUtils.ts @@ -16,10 +16,15 @@ limitations under the License. import {MatrixClientPeg} from '../MatrixClientPeg'; +const CALL_BEHAVIOUR_WK_KEY = "io.element.call_behaviour"; const E2EE_WK_KEY = "io.element.e2ee"; const E2EE_WK_KEY_DEPRECATED = "im.vector.riot.e2ee"; /* eslint-disable camelcase */ +export interface ICallBehaviourWellKnown { + widget_build_url?: string; +} + export interface IE2EEWellKnown { default?: boolean; secure_backup_required?: boolean; @@ -27,6 +32,11 @@ export interface IE2EEWellKnown { } /* eslint-enable camelcase */ +export function getCallBehaviourWellKnown(): ICallBehaviourWellKnown { + const clientWellKnown = MatrixClientPeg.get().getClientWellKnown(); + return clientWellKnown?.[CALL_BEHAVIOUR_WK_KEY]; +} + export function getE2EEWellKnown(): IE2EEWellKnown { const clientWellKnown = MatrixClientPeg.get().getClientWellKnown(); if (clientWellKnown && clientWellKnown[E2EE_WK_KEY]) { diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 815900d97a..db2b10b295 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -27,7 +27,7 @@ import {Room} from "matrix-js-sdk/src/models/room"; import {WidgetType} from "../widgets/WidgetType"; import {objectClone} from "./objects"; import {_t} from "../languageHandler"; -import {Capability, IWidgetData, MatrixCapabilities} from "matrix-widget-api"; +import {Capability, IWidget, IWidgetData, MatrixCapabilities} from "matrix-widget-api"; import {IApp} from "../stores/WidgetStore"; // How long we wait for the state event echo to come back from the server @@ -297,6 +297,16 @@ export default class WidgetUtils { content = {}; } + return WidgetUtils.setRoomWidgetContent(roomId, widgetId, content); + } + + static setRoomWidgetContent( + roomId: string, + widgetId: string, + content: IWidget, + ) { + const addingWidget = !!content.url; + WidgetEchoStore.setRoomWidgetEcho(roomId, widgetId, content); const client = MatrixClientPeg.get(); diff --git a/src/widgets/ManagedHybrid.ts b/src/widgets/ManagedHybrid.ts new file mode 100644 index 0000000000..f0abd48904 --- /dev/null +++ b/src/widgets/ManagedHybrid.ts @@ -0,0 +1,102 @@ +/* +Copyright 2021 The 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 { IWidget } from "matrix-widget-api"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { getCallBehaviourWellKnown } from "../utils/WellKnownUtils"; +import WidgetUtils from "../utils/WidgetUtils"; +import { IStoredLayout, WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; +import WidgetEchoStore from "../stores/WidgetEchoStore"; +import WidgetStore from "../stores/WidgetStore"; + +/* eslint-disable camelcase */ +interface IManagedHybridWidgetData { + widget_id: string; + widget: IWidget; + layout: IStoredLayout; +} +/* eslint-enable camelcase */ + +export function isManagedHybridWidgetEnabled(): boolean { + const callBehaviour = getCallBehaviourWellKnown(); + /* eslint-disable-next-line camelcase */ + return !!callBehaviour?.widget_build_url; +} + +export async function addManagedHybridWidget(roomId: string) { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(roomId); + if (!room) { + return; + } + + // Check for permission + if (!WidgetUtils.canUserModifyWidgets(roomId)) { + console.error(`User not allowed to modify widgets in ${roomId}`); + return; + } + + // Get widget data + /* eslint-disable-next-line camelcase */ + const widgetBuildUrl = getCallBehaviourWellKnown()?.widget_build_url; + if (!widgetBuildUrl) { + return; + } + let widgetData: IManagedHybridWidgetData; + try { + const response = await fetch(`${widgetBuildUrl}?roomId=${roomId}`); + widgetData = await response.json(); + } catch (e) { + console.error(`Managed hybrid widget builder failed for room ${roomId}`, e); + return; + } + if (!widgetData) { + return; + } + const { widget_id: widgetId, widget: widgetContent, layout } = widgetData; + + // Ensure the widget is not already present in the room + let widgets = WidgetStore.instance.getApps(roomId); + const existing = ( + widgets.some(w => w.id === widgetId) || + WidgetEchoStore.roomHasPendingWidgets(roomId, []) + ); + if (existing) { + console.error(`Managed hybrid widget already present in room ${roomId}`); + return; + } + + // Add the widget + try { + await WidgetUtils.setRoomWidgetContent(roomId, widgetId, widgetContent); + } catch (e) { + console.error(`Unable to add managed hybrid widget in room ${roomId}`, e); + return; + } + + // Move the widget into position + if (!WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) { + return; + } + widgets = WidgetStore.instance.getApps(roomId); + const installedWidget = widgets.find(w => w.id === widgetId); + if (!installedWidget) { + return; + } + WidgetLayoutStore.instance.moveToContainer(room, installedWidget, layout.container); + WidgetLayoutStore.instance.setContainerHeight(room, layout.container, layout.height); + WidgetLayoutStore.instance.copyLayoutToRoom(room); +}