diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 0558c48434..f6f6d22991 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -18,11 +18,9 @@ limitations under the License.
*/
import url from 'url';
-import qs from 'qs';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import WidgetMessaging from '../../../WidgetMessaging';
import AccessibleButton from './AccessibleButton';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
@@ -34,37 +32,15 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher/dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
-import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import PersistedElement from "./PersistedElement";
import {WidgetType} from "../../../widgets/WidgetType";
import {Capability} from "../../../widgets/WidgetApi";
-import {sleep} from "../../../utils/promise";
import {SettingLevel} from "../../../settings/SettingLevel";
import WidgetStore from "../../../stores/WidgetStore";
import {Action} from "../../../dispatcher/actions";
-
-const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
-const ENABLE_REACT_PERF = false;
-
-/**
- * Does template substitution on a URL (or any string). Variables will be
- * passed through encodeURIComponent.
- * @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'.
- * @param {Object} variables The key/value pairs to replace the template
- * variables with. E.g. { '$bar': 'baz' }.
- * @return {string} The result of replacing all template variables e.g. '/foo/baz'.
- */
-function uriFromTemplate(uriTemplate, variables) {
- let out = uriTemplate;
- for (const [key, val] of Object.entries(variables)) {
- out = out.replace(
- '$' + key, encodeURIComponent(val),
- );
- }
- return out;
-}
+import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
export default class AppTile extends React.Component {
constructor(props) {
@@ -72,6 +48,8 @@ export default class AppTile extends React.Component {
// The key used for PersistedElement
this._persistKey = 'widget_' + this.props.app.id;
+ this._sgWidget = new StopGapWidget(this.props);
+ this._sgWidget.on("ready", this._onWidgetReady);
this.state = this._getNewState(props);
@@ -123,43 +101,6 @@ export default class AppTile extends React.Component {
};
}
- /**
- * Does the widget support a given capability
- * @param {string} capability Capability to check for
- * @return {Boolean} True if capability supported
- */
- _hasCapability(capability) {
- return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
- }
-
- /**
- * Add widget instance specific parameters to pass in wUrl
- * Properties passed to widget instance:
- * - widgetId
- * - origin / parent URL
- * @param {string} urlString Url string to modify
- * @return {string}
- * Url string with parameters appended.
- * If url can not be parsed, it is returned unmodified.
- */
- _addWurlParams(urlString) {
- try {
- const parsed = new URL(urlString);
-
- // TODO: Replace these with proper widget params
- // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
- parsed.searchParams.set('widgetId', this.props.app.id);
- parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
-
- // Replace the encoded dollar signs back to dollar signs. They have no special meaning
- // in HTTP, but URL parsers encode them anyways.
- return parsed.toString().replace(/%24/g, '$');
- } catch (e) {
- console.error("Failed to add widget URL params:", e);
- return urlString;
- }
- }
-
isMixedContent() {
const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.app.url);
@@ -175,7 +116,7 @@ export default class AppTile extends React.Component {
componentDidMount() {
// Only fetch IM token on mount if we're showing and have permission to load
if (this.props.show && this.state.hasPermissionToLoad) {
- this.setScalarToken();
+ this._startWidget();
}
// Widget action listeners
@@ -191,80 +132,26 @@ export default class AppTile extends React.Component {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey);
}
+
+ if (this._sgWidget) {
+ this._sgWidget.stop();
+ }
}
- // TODO: Generify the name of this function. It's not just scalar tokens.
- /**
- * Adds a scalar token to the widget URL, if required
- * Component initialisation is only complete when this function has resolved
- */
- setScalarToken() {
- if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
- console.warn('Widget does not match integration manager, refusing to set auth token', url);
- this.setState({
- error: null,
- widgetUrl: this._addWurlParams(this.props.app.url),
- initialising: false,
- });
- return;
+ _resetWidget(newProps) {
+ if (this._sgWidget) {
+ this._sgWidget.stop();
}
+ this._sgWidget = new StopGapWidget(newProps);
+ this._sgWidget.on("ready", this._onWidgetReady);
+ this._startWidget();
+ }
- const managers = IntegrationManagers.sharedInstance();
- if (!managers.hasManager()) {
- console.warn("No integration manager - not setting scalar token", url);
- this.setState({
- error: null,
- widgetUrl: this._addWurlParams(this.props.app.url),
- initialising: false,
- });
- return;
- }
-
- // TODO: Pick the right manager for the widget
-
- const defaultManager = managers.getPrimaryManager();
- if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
- console.warn('Unknown integration manager, refusing to set auth token', url);
- this.setState({
- error: null,
- widgetUrl: this._addWurlParams(this.props.app.url),
- initialising: false,
- });
- return;
- }
-
- // Fetch the token before loading the iframe as we need it to mangle the URL
- if (!this._scalarClient) {
- this._scalarClient = defaultManager.getScalarClient();
- }
- this._scalarClient.getScalarToken().then((token) => {
- // Append scalar_token as a query param if not already present
- this._scalarClient.scalarToken = token;
- const u = url.parse(this._addWurlParams(this.props.app.url));
- const params = qs.parse(u.query);
- if (!params.scalar_token) {
- params.scalar_token = encodeURIComponent(token);
- // u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
- u.search = undefined;
- u.query = params;
+ _startWidget() {
+ this._sgWidget.prepare().then(() => {
+ if (this._appFrame.current) {
+ this._sgWidget.start(this._appFrame.current);
}
-
- this.setState({
- error: null,
- widgetUrl: u.format(),
- initialising: false,
- });
-
- // Fetch page title from remote content if not already set
- if (!this.state.widgetPageTitle && params.url) {
- this._fetchWidgetTitle(params.url);
- }
- }, (err) => {
- console.error("Failed to get scalar_token", err);
- this.setState({
- error: err.message,
- initialising: false,
- });
});
}
@@ -272,9 +159,8 @@ export default class AppTile extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
if (nextProps.app.url !== this.props.app.url) {
this._getNewState(nextProps);
- // Fetch IM token for new URL if we're showing and have permission to load
if (this.props.show && this.state.hasPermissionToLoad) {
- this.setScalarToken();
+ this._resetWidget(nextProps);
}
}
@@ -285,9 +171,9 @@ export default class AppTile extends React.Component {
loading: true,
});
}
- // Fetch IM token now that we're showing if we already have permission to load
+ // Start the widget now that we're showing if we already have permission to load
if (this.state.hasPermissionToLoad) {
- this.setScalarToken();
+ this._startWidget();
}
}
@@ -317,7 +203,14 @@ export default class AppTile extends React.Component {
}
_onSnapshotClick() {
- WidgetUtils.snapshotWidget(this.props.app);
+ this._sgWidget.widgetApi.takeScreenshot().then(data => {
+ dis.dispatch({
+ action: 'picture_snapshot',
+ file: data.screenshot,
+ });
+ }).catch(err => {
+ console.error("Failed to take screenshot: ", err);
+ });
}
/**
@@ -326,34 +219,23 @@ export default class AppTile extends React.Component {
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
*/
_endWidgetActions() {
- let terminationPromise;
-
- if (this._hasCapability(Capability.ReceiveTerminate)) {
- // Wait for widget to terminate within a timeout
- const timeout = 2000;
- const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
- terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
- } else {
- terminationPromise = Promise.resolve();
+ // HACK: This is a really dirty way to ensure that Jitsi cleans up
+ // its hold on the webcam. Without this, the widget holds a media
+ // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
+ if (this._appFrame.current) {
+ // In practice we could just do `+= ''` to trick the browser
+ // into thinking the URL changed, however I can foresee this
+ // being optimized out by a browser. Instead, we'll just point
+ // the iframe at a page that is reasonably safe to use in the
+ // event the iframe doesn't wink away.
+ // This is relative to where the Element instance is located.
+ this._appFrame.current.src = 'about:blank';
}
- return terminationPromise.finally(() => {
- // HACK: This is a really dirty way to ensure that Jitsi cleans up
- // its hold on the webcam. Without this, the widget holds a media
- // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
- if (this._appFrame.current) {
- // In practice we could just do `+= ''` to trick the browser
- // into thinking the URL changed, however I can foresee this
- // being optimized out by a browser. Instead, we'll just point
- // the iframe at a page that is reasonably safe to use in the
- // event the iframe doesn't wink away.
- // This is relative to where the Element instance is located.
- this._appFrame.current.src = 'about:blank';
- }
+ // Delete the widget from the persisted store for good measure.
+ PersistedElement.destroyElement(this._persistKey);
- // Delete the widget from the persisted store for good measure.
- PersistedElement.destroyElement(this._persistKey);
- });
+ this._sgWidget.stop();
}
/* If user has permission to modify widgets, delete the widget,
@@ -407,69 +289,18 @@ export default class AppTile extends React.Component {
this._revokeWidgetPermission();
}
- /**
- * Called when widget iframe has finished loading
- */
- _onLoaded() {
- // Destroy the old widget messaging before starting it back up again. Some widgets
- // have startup routines that run when they are loaded, so we just need to reinitialize
- // the messaging for them.
- ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
- this._setupWidgetMessaging();
-
- ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
+ _onWidgetReady = () => {
this.setState({loading: false});
- }
-
- _setupWidgetMessaging() {
- // FIXME: There's probably no reason to do this here: it should probably be done entirely
- // in ActiveWidgetStore.
- const widgetMessaging = new WidgetMessaging(
- this.props.app.id,
- this.props.app.url,
- this._getRenderedUrl(),
- this.props.userWidget,
- this._appFrame.current.contentWindow,
- );
- ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
- widgetMessaging.getCapabilities().then((requestedCapabilities) => {
- console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
- requestedCapabilities = requestedCapabilities || [];
-
- // Allow whitelisted capabilities
- let requestedWhitelistCapabilies = [];
-
- if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
- requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
- return this.indexOf(e)>=0;
- }, this.props.whitelistCapabilities);
-
- if (requestedWhitelistCapabilies.length > 0 ) {
- console.log(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
- requestedWhitelistCapabilies,
- );
- }
- }
-
- // TODO -- Add UI to warn about and optionally allow requested capabilities
-
- ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
-
- // 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 (WidgetType.JITSI.matches(this.props.app.type)) {
- widgetMessaging.flagReadyToContinue();
- }
- }).catch((err) => {
- console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err);
- });
- }
+ if (WidgetType.JITSI.matches(this.props.app.type)) {
+ this._sgWidget.widgetApi.transport.send("im.vector.ready", {});
+ }
+ };
_onAction(payload) {
if (payload.widgetId === this.props.app.id) {
switch (payload.action) {
case 'm.sticker':
- if (this._hasCapability('m.sticker')) {
+ if (this._sgWidget.widgetApi.hasCapability(Capability.Sticker)) {
dis.dispatch({action: 'post_sticker_message', data: payload.data});
} else {
console.warn('Ignoring sticker message. Invalid capability');
@@ -487,20 +318,6 @@ export default class AppTile extends React.Component {
}
}
- /**
- * Set remote content title on AppTile
- * @param {string} url Url to check for title
- */
- _fetchWidgetTitle(url) {
- this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
- if (widgetPageTitle) {
- this.setState({widgetPageTitle: widgetPageTitle});
- }
- }, (err) =>{
- console.error("Failed to get page title", err);
- });
- }
-
_grantWidgetPermission() {
const roomId = this.props.room.roomId;
console.info("Granting permission for widget to load: " + this.props.app.eventId);
@@ -510,7 +327,7 @@ export default class AppTile extends React.Component {
this.setState({hasPermissionToLoad: true});
// Fetch a token for the integration manager, now that we're allowed to
- this.setScalarToken();
+ this._startWidget();
}).catch(err => {
console.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
@@ -529,6 +346,7 @@ export default class AppTile extends React.Component {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
+ this._sgWidget.stop();
}).catch(err => {
console.error(err);
// We don't really need to do anything about this - the user will just hit the button again.
@@ -566,40 +384,6 @@ export default class AppTile extends React.Component {
}
}
- /**
- * Replace the widget template variables in a url with their values
- *
- * @param {string} u The URL with template variables
- * @param {string} widgetType The widget's type
- *
- * @returns {string} url with temlate variables replaced
- */
- _templatedUrl(u, widgetType: string) {
- const targetData = {};
- if (WidgetType.JITSI.matches(widgetType)) {
- targetData['domain'] = 'jitsi.riot.im'; // v1 jitsi widgets have this hardcoded
- }
- const myUserId = MatrixClientPeg.get().credentials.userId;
- const myUser = MatrixClientPeg.get().getUser(myUserId);
- const vars = Object.assign(targetData, this.props.app.data, {
- 'matrix_user_id': myUserId,
- 'matrix_room_id': this.props.room.roomId,
- 'matrix_display_name': myUser ? myUser.displayName : myUserId,
- 'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '',
-
- // TODO: Namespace themes through some standard
- 'theme': SettingsStore.getValue("theme"),
- });
-
- if (vars.conferenceId === undefined) {
- // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
- const parsedUrl = new URL(this.props.app.url);
- vars.conferenceId = parsedUrl.searchParams.get("confId");
- }
-
- return uriFromTemplate(u, vars);
- }
-
/**
* Whether we're using a local version of the widget rather than loading the
* actual widget URL
@@ -609,67 +393,11 @@ export default class AppTile extends React.Component {
return WidgetType.JITSI.matches(this.props.app.type);
}
- /**
- * Get the URL used in the iframe
- * In cases where we supply our own UI for a widget, this is an internal
- * URL different to the one used if the widget is popped out to a separate
- * tab / browser
- *
- * @returns {string} url
- */
- _getRenderedUrl() {
- let url;
-
- if (WidgetType.JITSI.matches(this.props.app.type)) {
- console.log("Replacing Jitsi widget URL with local wrapper");
- url = WidgetUtils.getLocalJitsiWrapperUrl({
- forLocalRender: true,
- auth: this.props.app.data ? this.props.app.data.auth : null,
- });
- url = this._addWurlParams(url);
- } else {
- url = this._getSafeUrl(this.state.widgetUrl);
- }
- return this._templatedUrl(url, this.props.app.type);
- }
-
- _getPopoutUrl() {
- if (WidgetType.JITSI.matches(this.props.app.type)) {
- return this._templatedUrl(
- WidgetUtils.getLocalJitsiWrapperUrl({
- forLocalRender: false,
- auth: this.props.app.data ? this.props.app.data.auth : null,
- }),
- this.props.app.type,
- );
- } else {
- // use app.url, not state.widgetUrl, because we want the one without
- // the wURL params for the popped-out version.
- return this._templatedUrl(this._getSafeUrl(this.props.app.url), this.props.app.type);
- }
- }
-
- _getSafeUrl(u) {
- const parsedWidgetUrl = url.parse(u, true);
- if (ENABLE_REACT_PERF) {
- parsedWidgetUrl.search = null;
- parsedWidgetUrl.query.react_perf = true;
- }
- let safeWidgetUrl = '';
- if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
- safeWidgetUrl = url.format(parsedWidgetUrl);
- }
-
- // Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
- // We also need the dollar signs in-tact for variable substitution.
- return safeWidgetUrl.replace(/%24/g, '$');
- }
-
_getTileTitle() {
const name = this.formatAppTileName();
const titleSpacer = - ;
let title = '';
- if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
+ if (this.state.widgetPageTitle && this.state.widgetPageTitle !== this.formatAppTileName()) {
title = this.state.widgetPageTitle;
}
@@ -694,7 +422,7 @@ export default class AppTile extends React.Component {
this._endWidgetActions().then(() => {
if (this._appFrame.current) {
// Reload iframe
- this._appFrame.current.src = this._getRenderedUrl();
+ this._appFrame.current.src = this._sgWidget.embedUrl;
this.setState({});
}
});
@@ -702,7 +430,7 @@ export default class AppTile extends React.Component {
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
- { target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
+ { target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click();
}
_onReloadWidgetClick() {
@@ -780,7 +508,7 @@ export default class AppTile extends React.Component {
@@ -827,9 +555,10 @@ export default class AppTile extends React.Component {
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
const canUserModify = this._canUserModify();
- const showEditButton = Boolean(this._scalarClient && canUserModify);
+ const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
- const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
+ const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(Capability.Screenshot)
+ && this.props.show;
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
contextMenu = (
diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts
index 1aa761e1c4..61387e3c26 100644
--- a/src/stores/OwnProfileStore.ts
+++ b/src/stores/OwnProfileStore.ts
@@ -66,12 +66,14 @@ export class OwnProfileStore extends AsyncStoreWithClient {
/**
* Gets the user's avatar as an HTTP URL of the given size. If the user's
* avatar is not present, this returns null.
- * @param size The size of the avatar
+ * @param size The size of the avatar. If zero, a full res copy of the avatar
+ * will be returned as an HTTP URL.
* @returns The HTTP URL of the user's avatar
*/
- public getHttpAvatarUrl(size: number): string {
+ public getHttpAvatarUrl(size: number = 0): string {
if (!this.avatarMxc) return null;
- return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size);
+ const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
+ return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
}
protected async onNotReady() {
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
new file mode 100644
index 0000000000..2b8ab9f5a8
--- /dev/null
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+
+import {Room} from "matrix-js-sdk/src/models/room";
+import { ClientWidgetApi, IWidget, IWidgetData, Widget } from "matrix-widget-api";
+import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
+import { EventEmitter } from "events";
+import { WidgetMessagingStore } from "./WidgetMessagingStore";
+import RoomViewStore from "../RoomViewStore";
+import { MatrixClientPeg } from "../../MatrixClientPeg";
+import { OwnProfileStore } from "../OwnProfileStore";
+import WidgetUtils from '../../utils/WidgetUtils';
+import { IntegrationManagers } from "../../integrations/IntegrationManagers";
+import SettingsStore from "../../settings/SettingsStore";
+import { WidgetType } from "../../widgets/WidgetType";
+
+// TODO: Destroy all of this code
+
+interface IAppTileProps {
+ // Note: these are only the props we care about
+
+ app: IWidget;
+ room: Room;
+ userId: string;
+ creatorUserId: string;
+ waitForIframeLoad: boolean;
+ whitelistCapabilities: string[];
+ userWidget: boolean;
+}
+
+// TODO: Don't use this because it's wrong
+class ElementWidget extends Widget {
+ constructor(w) {
+ super(w);
+ }
+
+ public get templateUrl(): string {
+ if (WidgetType.JITSI.matches(this.type)) {
+ return WidgetUtils.getLocalJitsiWrapperUrl({
+ forLocalRender: true,
+ auth: this.rawData?.auth,
+ });
+ }
+ return super.templateUrl;
+ }
+
+ public get rawData(): IWidgetData {
+ let conferenceId = super.rawData['conferenceId'];
+ if (conferenceId === undefined) {
+ // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
+ const parsedUrl = new URL(this.templateUrl);
+ conferenceId = parsedUrl.searchParams.get("confId");
+ }
+ return {
+ ...super.rawData,
+ theme: SettingsStore.getValue("theme"),
+ conferenceId,
+ };
+ }
+}
+
+export class StopGapWidget extends EventEmitter {
+ private messaging: ClientWidgetApi;
+ private mockWidget: Widget;
+ private scalarToken: string;
+
+ constructor(private appTileProps: IAppTileProps) {
+ super();
+ this.mockWidget = new ElementWidget(appTileProps.app);
+ }
+
+ public get widgetApi(): ClientWidgetApi {
+ return this.messaging;
+ }
+
+ /**
+ * The URL to use in the iframe
+ */
+ public get embedUrl(): string {
+ const templated = this.mockWidget.getCompleteUrl({
+ currentRoomId: RoomViewStore.getRoomId(),
+ currentUserId: MatrixClientPeg.get().getUserId(),
+ userDisplayName: OwnProfileStore.instance.displayName,
+ userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
+ });
+
+ // Add in some legacy support sprinkles
+ // TODO: Replace these with proper widget params
+ // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
+ const parsed = new URL(templated);
+ parsed.searchParams.set('widgetId', this.mockWidget.id);
+ parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
+
+ // Give the widget a scalar token if we're supposed to (more legacy)
+ // TODO: Stop doing this
+ if (this.scalarToken) {
+ parsed.searchParams.set('scalar_token', this.scalarToken);
+ }
+
+ // Replace the encoded dollar signs back to dollar signs. They have no special meaning
+ // in HTTP, but URL parsers encode them anyways.
+ return parsed.toString().replace(/%24/g, '$');
+ }
+
+ /**
+ * The URL to use in the popout
+ */
+ public get popoutUrl(): string {
+ if (WidgetType.JITSI.matches(this.mockWidget.type)) {
+ return WidgetUtils.getLocalJitsiWrapperUrl({
+ forLocalRender: false,
+ auth: this.mockWidget.rawData?.auth,
+ });
+ }
+ return this.embedUrl;
+ }
+
+ public get isManagedByManager(): boolean {
+ return !!this.scalarToken;
+ }
+
+ public get started(): boolean {
+ return !!this.messaging;
+ }
+
+ public start(iframe: HTMLIFrameElement) {
+ if (this.started) return;
+ const driver = new StopGapWidgetDriver(this.appTileProps.whitelistCapabilities || []);
+ this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
+ this.messaging.addEventListener("ready", () => this.emit("ready"));
+ WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
+ }
+
+ public async prepare(): Promise {
+ if (this.scalarToken) return;
+ try {
+ if (WidgetUtils.isScalarUrl(this.mockWidget.templateUrl)) {
+ const managers = IntegrationManagers.sharedInstance();
+ if (managers.hasManager()) {
+ // TODO: Pick the right manager for the widget
+ const defaultManager = managers.getPrimaryManager();
+ if (WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
+ const scalar = defaultManager.getScalarClient();
+ this.scalarToken = await scalar.getScalarToken();
+ }
+ }
+ }
+ } catch (e) {
+ // All errors are non-fatal
+ console.error("Error preparing widget communications: ", e);
+ }
+ }
+
+ public stop() {
+ if (!this.started) return;
+ WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
+ }
+}
diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
new file mode 100644
index 0000000000..84626e74fb
--- /dev/null
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+import { Capability, WidgetDriver } from "matrix-widget-api";
+import { iterableUnion } from "../../utils/iterables";
+
+// TODO: Purge this from the universe
+
+export class StopGapWidgetDriver extends WidgetDriver {
+ constructor(private allowedCapabilities: Capability[]) {
+ super();
+ }
+
+ public async validateCapabilities(requested: Set): Promise> {
+ return iterableUnion(requested, new Set(this.allowedCapabilities));
+ }
+}
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index fedc9c6c87..fa743fdeaf 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -31,8 +31,7 @@ import { EnhancedMap } from "../../utils/maps";
export class WidgetMessagingStore extends AsyncStoreWithClient {
private static internalInstance = new WidgetMessagingStore();
- // >
- private widgetMap = new EnhancedMap>();
+ private widgetMap = new EnhancedMap(); //
public constructor() {
super(defaultDispatcher);
@@ -51,106 +50,16 @@ export class WidgetMessagingStore extends AsyncStoreWithClient {
this.widgetMap.clear();
}
- /**
- * Finds a widget by ID. Not guaranteed to return an accurate result.
- * @param {string} id The widget ID.
- * @returns {{widget, room}} The widget and possible room ID, or a falsey value
- * if not found.
- * @deprecated Do not use.
- */
- public findWidgetById(id: string): { widget: Widget, room?: Room } {
- for (const key of this.widgetMap.keys()) {
- for (const [entityId, surrogate] of this.widgetMap.get(key).entries()) {
- if (surrogate.definition.id === id) {
- const room: Room = this.matrixClient?.getRoom(entityId); // will be null for non-rooms
- return {room, widget: surrogate.definition};
- }
- }
- }
- return null;
+ public storeMessaging(widget: Widget, widgetApi: ClientWidgetApi) {
+ this.stopMessaging(widget);
+ this.widgetMap.set(widget.id, widgetApi);
}
- /**
- * Gets the messaging instance for the widget. Returns a falsey value if none
- * is present.
- * @param {Room} room The room for which the widget lives within.
- * @param {Widget} widget The widget to get messaging for.
- * @returns {ClientWidgetApi} The messaging, or a falsey value.
- */
- public messagingForRoomWidget(room: Room, widget: Widget): ClientWidgetApi {
- return this.widgetMap.get(room.roomId)?.get(widget.id)?.messaging;
+ public stopMessaging(widget: Widget) {
+ this.widgetMap.remove(widget.id)?.stop();
}
- /**
- * Gets the messaging instance for the widget. Returns a falsey value if none
- * is present.
- * @param {Widget} widget The widget to get messaging for.
- * @returns {ClientWidgetApi} The messaging, or a falsey value.
- */
- public messagingForAccountWidget(widget: Widget): ClientWidgetApi {
- return this.widgetMap.get(this.matrixClient?.getUserId())?.get(widget.id)?.messaging;
- }
-
- private generateMessaging(locationId: string, widget: Widget, iframe: HTMLIFrameElement, driver: WidgetDriver) {
- const messaging = new ClientWidgetApi(widget, iframe, driver);
- this.widgetMap.getOrCreate(locationId, new EnhancedMap())
- .getOrCreate(widget.id, new WidgetSurrogate(widget, messaging));
- return messaging;
- }
-
- /**
- * Generates a messaging instance for the widget. If an instance already exists, it
- * will be returned instead.
- * @param {Room} room The room in which the widget lives.
- * @param {Widget} widget The widget to generate/get messaging for.
- * @param {HTMLIFrameElement} iframe The widget's iframe.
- * @returns {ClientWidgetApi} The generated/cached messaging.
- */
- public generateMessagingForRoomWidget(room: Room, widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi {
- const existing = this.messagingForRoomWidget(room, widget);
- if (existing) return existing;
-
- const driver = new SdkWidgetDriver(widget, WidgetKind.Room, room.roomId);
- return this.generateMessaging(room.roomId, widget, iframe, driver);
- }
-
- /**
- * Generates a messaging instance for the widget. If an instance already exists, it
- * will be returned instead.
- * @param {Widget} widget The widget to generate/get messaging for.
- * @param {HTMLIFrameElement} iframe The widget's iframe.
- * @returns {ClientWidgetApi} The generated/cached messaging.
- */
- public generateMessagingForAccountWidget(widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi {
- if (!this.matrixClient) {
- throw new Error("No matrix client to create account widgets with");
- }
-
- const existing = this.messagingForAccountWidget(widget);
- if (existing) return existing;
-
- const userId = this.matrixClient.getUserId();
- const driver = new SdkWidgetDriver(widget, WidgetKind.Account, userId);
- return this.generateMessaging(userId, widget, iframe, driver);
- }
-
- /**
- * Stops the messaging instance for the widget, unregistering it.
- * @param {Room} room The room where the widget resides.
- * @param {Widget} widget The widget
- */
- public stopMessagingForRoomWidget(room: Room, widget: Widget) {
- const api = this.widgetMap.getOrCreate(room.roomId, new EnhancedMap()).remove(widget.id);
- if (api) api.messaging.stop();
- }
-
- /**
- * Stops the messaging instance for the widget, unregistering it.
- * @param {Widget} widget The widget
- */
- public stopMessagingForAccountWidget(widget: Widget) {
- if (!this.matrixClient) return;
- const api = this.widgetMap.getOrCreate(this.matrixClient.getUserId(), new EnhancedMap()).remove(widget.id);
- if (api) api.messaging.stop();
+ public getMessaging(widget: Widget): ClientWidgetApi {
+ return this.widgetMap.get(widget.id);
}
}
diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index d1daba7ca5..57459ba897 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -424,7 +424,6 @@ export default class WidgetUtils {
if (WidgetType.JITSI.matches(appType)) {
capWhitelist.push(Capability.AlwaysOnScreen);
}
- capWhitelist.push(Capability.ReceiveTerminate);
return capWhitelist;
}
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
index c25d607948..ab9604d155 100644
--- a/src/widgets/WidgetApi.ts
+++ b/src/widgets/WidgetApi.ts
@@ -25,7 +25,6 @@ export enum Capability {
Screenshot = "m.capability.screenshot",
Sticker = "m.sticker",
AlwaysOnScreen = "m.always_on_screen",
- ReceiveTerminate = "im.vector.receive_terminate",
}
export enum KnownWidgetActions {