diff --git a/src/CallHandler.js b/src/CallHandler.js index c63bfe309a..2bfe10850a 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -66,6 +66,7 @@ import WidgetEchoStore from './stores/WidgetEchoStore'; import SettingsStore, { SettingLevel } from './settings/SettingsStore'; import {generateHumanReadableId} from "./utils/NamingUtils"; import {Jitsi} from "./widgets/Jitsi"; +import {WidgetType} from "./widgets/WidgetType"; global.mxCalls = { //room_id: MatrixCall @@ -401,9 +402,9 @@ async function _startCallApp(roomId, type) { }); const room = MatrixClientPeg.get().getRoom(roomId); - const currentRoomWidgets = WidgetUtils.getRoomWidgets(room); + const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI); - if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, 'jitsi')) { + if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { @@ -413,9 +414,6 @@ async function _startCallApp(roomId, type) { return; } - const currentJitsiWidgets = currentRoomWidgets.filter((ev) => { - return ev.getContent().type === 'jitsi'; - }); if (currentJitsiWidgets.length > 0) { console.warn( "Refusing to start conference call widget in " + roomId + @@ -454,7 +452,7 @@ async function _startCallApp(roomId, type) { Date.now() ); - WidgetUtils.setRoomWidget(roomId, widgetId, 'jitsi', widgetUrl, 'Jitsi', widgetData).then(() => { + WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => { console.log('Jitsi widget added'); }).catch((e) => { if (e.errcode === 'M_FORBIDDEN') { diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index ca8ca103e1..607f702c73 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -241,6 +241,7 @@ import WidgetUtils from './utils/WidgetUtils'; import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; import {IntegrationManagers} from "./integrations/IntegrationManagers"; +import {WidgetType} from "./widgets/WidgetType"; function sendResponse(event, res) { const data = JSON.parse(JSON.stringify(event.data)); @@ -292,7 +293,7 @@ function inviteUser(event, roomId, userId) { function setWidget(event, roomId) { const widgetId = event.data.widget_id; - const widgetType = event.data.type; + let widgetType = event.data.type; const widgetUrl = event.data.url; const widgetName = event.data.name; // optional const widgetData = event.data.data; // optional @@ -324,6 +325,9 @@ function setWidget(event, roomId) { } } + // convert the widget type to a known widget type + widgetType = WidgetType.fromString(widgetType); + if (userWidget) { WidgetUtils.setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => { sendResponse(event, { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 76472e4d66..5e8c1087f7 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -35,6 +35,7 @@ import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks"; import {inviteUsersToRoom} from "./RoomInvite"; +import { WidgetType } from "./widgets/WidgetType"; import sendBugReport from "./rageshake/submit-rageshake"; import SdkConfig from "./SdkConfig"; @@ -779,7 +780,7 @@ export const Commands = [ const nowMs = (new Date()).getTime(); const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`); return success(WidgetUtils.setRoomWidget( - roomId, widgetId, "m.custom", args, "Custom Widget", {})); + roomId, widgetId, WidgetType.CUSTOM, args, "Custom Widget", {})); } else { return reject(_t("You cannot modify widgets in this room.")); } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 73ed605edd..58bfda8d22 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -38,6 +38,7 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; import PersistedElement from "./PersistedElement"; +import {WidgetType} from "../../../widgets/WidgetType"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -454,7 +455,7 @@ export default class AppTile extends React.Component { // We only tell Jitsi widgets that we're ready because they're realistically the only ones // using this custom extension to the widget API. - if (this.props.app.type === 'jitsi') { + if (WidgetType.JITSI.matches(this.props.app.type)) { widgetMessaging.flagReadyToContinue(); } }).catch((err) => { @@ -597,7 +598,7 @@ export default class AppTile extends React.Component { _getRenderedUrl() { let url; - if (this.props.app.type === 'jitsi') { + if (WidgetType.JITSI.matches(this.props.app.type)) { console.log("Replacing Jitsi widget URL with local wrapper"); url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true}); url = this._addWurlParams(url); @@ -608,7 +609,7 @@ export default class AppTile extends React.Component { } _getPopoutUrl() { - if (this.props.app.type === 'jitsi') { + if (WidgetType.JITSI.matches(this.props.app.type)) { return this._templatedUrl( WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}), ); diff --git a/src/stores/WidgetEchoStore.js b/src/stores/WidgetEchoStore.js index 33fa45c635..a5e7b12da0 100644 --- a/src/stores/WidgetEchoStore.js +++ b/src/stores/WidgetEchoStore.js @@ -16,6 +16,7 @@ limitations under the License. */ import EventEmitter from 'events'; +import {WidgetType} from "../widgets/WidgetType"; /** * Acts as a place to get & set widget state, storing local echo state and @@ -64,7 +65,7 @@ class WidgetEchoStore extends EventEmitter { return echoedWidgets; } - roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, type) { + roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, type: WidgetType) { const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]); // any widget IDs that are already in the room are not pending, so @@ -79,7 +80,7 @@ class WidgetEchoStore extends EventEmitter { return Object.keys(roomEchoState).length > 0; } else { return Object.values(roomEchoState).some((widget) => { - return widget.type === type; + return type.matches(widget.type); }); } } diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 6768119d8f..6a0556c2b3 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -29,6 +29,8 @@ import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {Capability} from "../widgets/WidgetApi"; +import {Room} from "matrix-js-sdk/src/models/room"; +import {WidgetType} from "../widgets/WidgetType"; export default class WidgetUtils { /* Returns true if user is able to send state events to modify widgets in this room @@ -252,14 +254,16 @@ export default class WidgetUtils { }); } - static setRoomWidget(roomId, widgetId, widgetType, widgetUrl, widgetName, widgetData) { + static setRoomWidget(roomId, widgetId, widgetType: WidgetType, widgetUrl, widgetName, widgetData) { let content; const addingWidget = Boolean(widgetUrl); if (addingWidget) { content = { - type: widgetType, + // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111) + // For now we'll send the legacy event type for compatibility with older apps/riots + type: widgetType.legacy, url: widgetUrl, name: widgetName, data: widgetData, @@ -281,10 +285,10 @@ export default class WidgetUtils { /** * Get room specific widgets - * @param {object} room The room to get widgets force + * @param {Room} room The room to get widgets force * @return {[object]} Array containing current / active room widgets */ - static getRoomWidgets(room) { + static getRoomWidgets(room: Room) { // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111) const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); if (!appsStateEvents) { @@ -338,6 +342,14 @@ export default class WidgetUtils { return widgets.filter(w => w.content && w.content.type === "m.integration_manager"); } + static getRoomWidgetsOfType(room: Room, type: WidgetType) { + const widgets = WidgetUtils.getRoomWidgets(room); + return (widgets || []).filter(w => { + const content = w.getContent(); + return content.url && type.matches(content.type); + }); + } + static removeIntegrationManagerWidgets() { const client = MatrixClientPeg.get(); if (!client) { @@ -405,7 +417,7 @@ export default class WidgetUtils { // Obviously anyone that can add a widget can claim it's a jitsi widget, // so this doesn't really offer much over the set of domains we load // widgets from at all, but it probably makes sense for sanity. - if (appType === 'jitsi') { + if (WidgetType.JITSI.matches(appType)) { capWhitelist.push(Capability.AlwaysOnScreen); } diff --git a/src/widgets/WidgetType.ts b/src/widgets/WidgetType.ts new file mode 100644 index 0000000000..09c30430dd --- /dev/null +++ b/src/widgets/WidgetType.ts @@ -0,0 +1,37 @@ +/* +Copyright 2020 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. +*/ + +export class WidgetType { + public static readonly JITSI = new WidgetType("m.jitsi", "jitsi"); + public static readonly CUSTOM = new WidgetType("m.custom", "m.custom"); + + constructor(public readonly preferred: string, public readonly legacy: string) { + } + + public matches(type: string): boolean { + return type === this.preferred || type === this.legacy; + } + + static fromString(type: string): WidgetType { + // First try and match it against something we're already aware of + const known = Object.values(WidgetType).filter(v => v instanceof WidgetType); + const knownMatch = known.find(w => w.matches(type)); + if (knownMatch) return knownMatch; + + // If that fails, invent a new widget type + return new WidgetType(type, type); + } +}