From dc92f557fd8ec8e65b7088a006dd993071c002ed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 9 Apr 2020 15:11:57 -0600 Subject: [PATCH 1/2] Support m.jitsi-typed widgets as Jitsi widgets Fixes https://github.com/vector-im/riot-web/issues/9268 --- src/CallHandler.js | 10 ++++----- src/components/views/elements/AppTile.js | 7 ++++--- src/stores/WidgetEchoStore.js | 5 +++-- src/utils/WidgetUtils.js | 22 +++++++++++++++----- src/widgets/WidgetType.ts | 26 ++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 src/widgets/WidgetType.ts 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/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 9fb6358c1f..bebed54438 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 @@ -249,14 +251,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, @@ -279,10 +283,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) { const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); if (!appsStateEvents) { return []; @@ -335,6 +339,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) { @@ -402,7 +414,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..7608819bb9 --- /dev/null +++ b/src/widgets/WidgetType.ts @@ -0,0 +1,26 @@ +/* +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"); + + constructor(public readonly preferred: string, public readonly legacy: string) { + } + + public matches(type: string): boolean { + return type === this.preferred || type === this.legacy; + } +} From 4510499987b8244f24b41e958dc1990f39bebf7c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 9 Apr 2020 15:25:11 -0600 Subject: [PATCH 2/2] Fix widgets for all other sources too --- src/ScalarMessaging.js | 6 +++++- src/SlashCommands.tsx | 3 ++- src/widgets/WidgetType.ts | 11 +++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 2211e513c3..9be7730c3a 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -239,6 +239,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)); @@ -290,7 +291,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 @@ -322,6 +323,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 d60434cf97..0afea63a78 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"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -775,7 +776,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/widgets/WidgetType.ts b/src/widgets/WidgetType.ts index 7608819bb9..09c30430dd 100644 --- a/src/widgets/WidgetType.ts +++ b/src/widgets/WidgetType.ts @@ -16,6 +16,7 @@ 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) { } @@ -23,4 +24,14 @@ export class WidgetType { 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); + } }