From 999b5afa0ad9c40a810a39dd2d77a92ed8ab6b09 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 4 Sep 2020 21:41:14 -0600
Subject: [PATCH 01/21] Acknowledge the visibility request

---
 src/FromWidgetPostMessageApi.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
index d5d7c08d50..bbccc47d28 100644
--- a/src/FromWidgetPostMessageApi.js
+++ b/src/FromWidgetPostMessageApi.js
@@ -218,6 +218,9 @@ export default class FromWidgetPostMessageApi {
             if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
                 ActiveWidgetStore.setWidgetPersistence(widgetId, val);
             }
+
+            // acknowledge
+            this.sendResponse(event, {});
         } else if (action === 'get_openid') {
             // Handled by caller
         } else {

From 634ffb0140d2e13e6b9af32400e024ead6b5c577 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 25 Sep 2020 09:39:21 -0600
Subject: [PATCH 02/21] Add structure for widget messaging layer

---
 src/stores/widgets/SdkWidgetDriver.ts      |  34 ++++++
 src/stores/widgets/WidgetMessagingStore.ts | 117 +++++++++++++++++++++
 src/stores/widgets/WidgetSurrogate.ts      |  25 +++++
 src/utils/iterables.ts                     |  21 ++++
 src/utils/maps.ts                          |  17 +++
 5 files changed, 214 insertions(+)
 create mode 100644 src/stores/widgets/SdkWidgetDriver.ts
 create mode 100644 src/stores/widgets/WidgetMessagingStore.ts
 create mode 100644 src/stores/widgets/WidgetSurrogate.ts
 create mode 100644 src/utils/iterables.ts

diff --git a/src/stores/widgets/SdkWidgetDriver.ts b/src/stores/widgets/SdkWidgetDriver.ts
new file mode 100644
index 0000000000..1462303fa3
--- /dev/null
+++ b/src/stores/widgets/SdkWidgetDriver.ts
@@ -0,0 +1,34 @@
+/*
+ * 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, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api";
+import { iterableUnion } from "../../utils/iterables";
+
+export class SdkWidgetDriver extends WidgetDriver {
+    public constructor(
+        private widget: Widget,
+        private widgetKind: WidgetKind,
+        private locationEntityId: string,
+        private preapprovedCapabilities: Set<Capability> = new Set(),
+    ) {
+        super();
+    }
+
+    public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
+        // TODO: Prompt the user to accept capabilities
+        return iterableUnion(requested, this.preapprovedCapabilities);
+    }
+}
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
new file mode 100644
index 0000000000..6d05cae8c6
--- /dev/null
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -0,0 +1,117 @@
+/*
+ * 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 { ClientWidgetApi, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api";
+import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import { ActionPayload } from "../../dispatcher/payloads";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { WidgetSurrogate } from "./WidgetSurrogate";
+import { SdkWidgetDriver } from "./SdkWidgetDriver";
+import { EnhancedMap } from "../../utils/maps";
+
+/**
+ * Temporary holding store for widget messaging instances. This is eventually
+ * going to be merged with a more complete WidgetStore, but for now it's
+ * easiest to split this into a single place.
+ */
+export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
+    private static internalInstance = new WidgetMessagingStore();
+
+    // <room/user ID, <widget ID, Widget>>
+    private widgetMap = new EnhancedMap<string, EnhancedMap<string, WidgetSurrogate>>();
+
+    public constructor() {
+        super(defaultDispatcher);
+    }
+
+    public static get instance(): WidgetMessagingStore {
+        return WidgetMessagingStore.internalInstance;
+    }
+
+    protected async onAction(payload: ActionPayload): Promise<any> {
+        // nothing to do
+    }
+
+    protected async onReady(): Promise<any> {
+        // just in case
+        this.widgetMap.clear();
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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);
+    }
+}
diff --git a/src/stores/widgets/WidgetSurrogate.ts b/src/stores/widgets/WidgetSurrogate.ts
new file mode 100644
index 0000000000..4d482124a6
--- /dev/null
+++ b/src/stores/widgets/WidgetSurrogate.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { ClientWidgetApi, Widget } from "matrix-widget-api";
+
+export class WidgetSurrogate {
+    public constructor(
+        public readonly definition: Widget,
+        public readonly messaging: ClientWidgetApi,
+    ) {
+    }
+}
diff --git a/src/utils/iterables.ts b/src/utils/iterables.ts
new file mode 100644
index 0000000000..3d2585906d
--- /dev/null
+++ b/src/utils/iterables.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { arrayUnion } from "./arrays";
+
+export function iterableUnion<C extends Iterable<T>, T>(a: C, b: C): Set<T> {
+    return new Set(arrayUnion(Array.from(a), Array.from(b)));
+}
diff --git a/src/utils/maps.ts b/src/utils/maps.ts
index 96832094f0..630e0af286 100644
--- a/src/utils/maps.ts
+++ b/src/utils/maps.ts
@@ -44,3 +44,20 @@ export function mapKeyChanges<K, V>(a: Map<K, V>, b: Map<K, V>): K[] {
     const diff = mapDiff(a, b);
     return arrayMerge(diff.removed, diff.added, diff.changed);
 }
+
+/**
+ * A Map<K, V> with added utility.
+ */
+export class EnhancedMap<K, V> extends Map<K, V> {
+    public constructor(entries?: Iterable<[K, V]>) {
+        super(entries);
+    }
+
+    public getOrCreate(key: K, def: V): V {
+        if (this.has(key)) {
+            return this.get(key);
+        }
+        this.set(key, def);
+        return def;
+    }
+}

From 96fa34eecfc251507b9e4788a3cdcb1214694d40 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Sat, 26 Sep 2020 18:40:26 -0600
Subject: [PATCH 03/21] Add stop functions

---
 src/stores/widgets/WidgetMessagingStore.ts | 20 ++++++++++++++++++++
 src/utils/maps.ts                          |  6 ++++++
 2 files changed, 26 insertions(+)

diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index 6d05cae8c6..dfa8eed943 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -114,4 +114,24 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
         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();
+    }
 }
diff --git a/src/utils/maps.ts b/src/utils/maps.ts
index 630e0af286..57d84bd33f 100644
--- a/src/utils/maps.ts
+++ b/src/utils/maps.ts
@@ -60,4 +60,10 @@ export class EnhancedMap<K, V> extends Map<K, V> {
         this.set(key, def);
         return def;
     }
+
+    public remove(key: K): V {
+        const v = this.get(key);
+        this.delete(key);
+        return v;
+    }
 }

From 4ea3376abf76b72c307da1fefd4569c3a9b1c03c Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 28 Sep 2020 13:34:13 -0600
Subject: [PATCH 04/21] WIP on AppTile2 transformation

---
 src/components/views/elements/AppTile.js   |  2 -
 src/components/views/elements/AppTile2.tsx | 77 ++++++++++++++++++++++
 src/stores/ActiveWidgetStore.js            | 60 +++--------------
 src/stores/widgets/WidgetMessagingStore.ts | 19 ++++++
 4 files changed, 106 insertions(+), 52 deletions(-)
 create mode 100644 src/components/views/elements/AppTile2.tsx

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 6aaeab060f..83dd9d7b1e 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -108,7 +108,6 @@ export default class AppTile extends React.Component {
             return !!currentlyAllowedWidgets[newProps.app.eventId];
         };
 
-        const PersistedElement = sdk.getComponent("elements.PersistedElement");
         return {
             initialising: true, // True while we are mangling the widget URL
             // True while the iframe content is loading
@@ -190,7 +189,6 @@ export default class AppTile extends React.Component {
         // if it's not remaining on screen, get rid of the PersistedElement container
         if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
             ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
-            const PersistedElement = sdk.getComponent("elements.PersistedElement");
             PersistedElement.destroyElement(this._persistKey);
         }
     }
diff --git a/src/components/views/elements/AppTile2.tsx b/src/components/views/elements/AppTile2.tsx
new file mode 100644
index 0000000000..78bb6f7754
--- /dev/null
+++ b/src/components/views/elements/AppTile2.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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 { ClientWidgetApi, Widget, WidgetKind } from "matrix-widget-api";
+import * as React from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
+
+interface IProps {
+    widget: Widget;
+    kind: WidgetKind;
+    room?: Room;
+
+    // TODO: All the showUIElement props
+}
+
+interface IState {
+    loading: boolean;
+}
+
+export class AppTile2 extends React.PureComponent<IProps, IState> {
+    private messaging: ClientWidgetApi;
+    private iframeRef = React.createRef<HTMLIFrameElement>();
+
+    public constructor(props: IProps) {
+        super(props);
+
+        if (props.kind === WidgetKind.Room && !props.room) {
+            throw new Error("Expected room when supplied with a room widget");
+        }
+
+        this.state = {
+            loading: true,
+        };
+    }
+
+    private get isMixedContent(): boolean {
+        const myProtocol = window.location.protocol;
+        const widgetProtocol = new URL(this.props.widget.templateUrl).protocol;
+        return myProtocol === 'https:' && widgetProtocol !== 'https:';
+    }
+
+    public componentDidMount() {
+        if (!this.iframeRef.current) {
+            throw new Error("iframe has not yet been associated - fix the render code");
+        }
+
+        // TODO: Provide capabilities to widget messaging
+
+        if (this.props.kind === WidgetKind.Room) {
+            this.messaging = WidgetMessagingStore.instance
+                .generateMessagingForRoomWidget(this.props.room, this.props.widget, this.iframeRef.current);
+        } else if (this.props.kind === WidgetKind.Account) {
+            this.messaging = WidgetMessagingStore.instance
+                .generateMessagingForAccountWidget(this.props.widget, this.iframeRef.current);
+        } else {
+            throw new Error("Unexpected widget kind: " + this.props.kind);
+        }
+
+        this.messaging.once("ready", () => {
+            this.setState({loading: false});
+        });
+    }
+}
diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js
index bf9ae3586c..d6aaf83196 100644
--- a/src/stores/ActiveWidgetStore.js
+++ b/src/stores/ActiveWidgetStore.js
@@ -17,6 +17,7 @@ limitations under the License.
 import EventEmitter from 'events';
 
 import {MatrixClientPeg} from '../MatrixClientPeg';
+import {WidgetMessagingStore} from "./widgets/WidgetMessagingStore";
 
 /**
  * Stores information about the widgets active in the app right now:
@@ -29,15 +30,6 @@ class ActiveWidgetStore extends EventEmitter {
         super();
         this._persistentWidgetId = null;
 
-        // A list of negotiated capabilities for each widget, by ID
-        // {
-        //     widgetId: [caps...],
-        // }
-        this._capsByWidgetId = {};
-
-        // A WidgetMessaging instance for each widget ID
-        this._widgetMessagingByWidgetId = {};
-
         // What room ID each widget is associated with (if it's a room widget)
         this._roomIdByWidgetId = {};
 
@@ -54,8 +46,6 @@ class ActiveWidgetStore extends EventEmitter {
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
         }
-        this._capsByWidgetId = {};
-        this._widgetMessagingByWidgetId = {};
         this._roomIdByWidgetId = {};
     }
 
@@ -76,9 +66,16 @@ class ActiveWidgetStore extends EventEmitter {
         if (id !== this._persistentWidgetId) return;
         const toDeleteId = this._persistentWidgetId;
 
+        const result = WidgetMessagingStore.instance.findWidgetById(id);
+        if (result) {
+            if (result.room) {
+                WidgetMessagingStore.instance.stopMessagingForRoomWidget(result.room, result.widget);
+            } else {
+                WidgetMessagingStore.instance.stopMessagingForAccountWidget(result.widget);
+            }
+        }
+
         this.setWidgetPersistence(toDeleteId, false);
-        this.delWidgetMessaging(toDeleteId);
-        this.delWidgetCapabilities(toDeleteId);
         this.delRoomId(toDeleteId);
     }
 
@@ -99,43 +96,6 @@ class ActiveWidgetStore extends EventEmitter {
         return this._persistentWidgetId;
     }
 
-    setWidgetCapabilities(widgetId, caps) {
-        this._capsByWidgetId[widgetId] = caps;
-        this.emit('update');
-    }
-
-    widgetHasCapability(widgetId, cap) {
-        return this._capsByWidgetId[widgetId] && this._capsByWidgetId[widgetId].includes(cap);
-    }
-
-    delWidgetCapabilities(widgetId) {
-        delete this._capsByWidgetId[widgetId];
-        this.emit('update');
-    }
-
-    setWidgetMessaging(widgetId, wm) {
-        // Stop any existing widget messaging first
-        this.delWidgetMessaging(widgetId);
-        this._widgetMessagingByWidgetId[widgetId] = wm;
-        this.emit('update');
-    }
-
-    getWidgetMessaging(widgetId) {
-        return this._widgetMessagingByWidgetId[widgetId];
-    }
-
-    delWidgetMessaging(widgetId) {
-        if (this._widgetMessagingByWidgetId[widgetId]) {
-            try {
-                this._widgetMessagingByWidgetId[widgetId].stop();
-            } catch (e) {
-                console.error('Failed to stop listening for widgetMessaging events', e.message);
-            }
-            delete this._widgetMessagingByWidgetId[widgetId];
-            this.emit('update');
-        }
-    }
-
     getRoomId(widgetId) {
         return this._roomIdByWidgetId[widgetId];
     }
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index dfa8eed943..fedc9c6c87 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -51,6 +51,25 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
         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;
+    }
+
     /**
      * Gets the messaging instance for the widget. Returns a falsey value if none
      * is present.

From 6b2e34dc0045a23f52e298f5b5f1d67e8b468e28 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 28 Sep 2020 21:14:50 -0600
Subject: [PATCH 05/21] Fix export

---
 src/components/views/elements/AppTile2.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/AppTile2.tsx b/src/components/views/elements/AppTile2.tsx
index 78bb6f7754..516c00170a 100644
--- a/src/components/views/elements/AppTile2.tsx
+++ b/src/components/views/elements/AppTile2.tsx
@@ -31,7 +31,7 @@ interface IState {
     loading: boolean;
 }
 
-export class AppTile2 extends React.PureComponent<IProps, IState> {
+export default class AppTile2 extends React.PureComponent<IProps, IState> {
     private messaging: ClientWidgetApi;
     private iframeRef = React.createRef<HTMLIFrameElement>();
 

From 78a04a610662ea0071de3f77b0ab41d0bef6e3ae Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 28 Sep 2020 21:23:55 -0600
Subject: [PATCH 06/21] Remove unused prop

---
 src/components/views/elements/AppTile.js | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 83dd9d7b1e..0558c48434 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -455,10 +455,6 @@ export default class AppTile extends React.Component {
 
             ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
 
-            if (this.props.onCapabilityRequest) {
-                this.props.onCapabilityRequest(requestedCapabilities);
-            }
-
             // 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)) {
@@ -941,9 +937,6 @@ AppTile.propTypes = {
     // NOTE -- Use with caution. This is intended to aid better integration / UX
     // basic widget capabilities, e.g. injecting sticker message events.
     whitelistCapabilities: PropTypes.array,
-    // Optional function to be called on widget capability request
-    // Called with an array of the requested capabilities
-    onCapabilityRequest: PropTypes.func,
     // Is this an instance of a user widget
     userWidget: PropTypes.bool,
 };

From cd93b2c22ad951ed3ee50ae56cadb40efe49a620 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 14:14:51 -0600
Subject: [PATCH 07/21] First rough cut of cutting AppTile over to the
 ClientWidgetApi

---
 src/components/views/elements/AppTile.js   | 389 ++++-----------------
 src/stores/OwnProfileStore.ts              |   8 +-
 src/stores/widgets/StopGapWidget.ts        | 171 +++++++++
 src/stores/widgets/StopGapWidgetDriver.ts  |  30 ++
 src/stores/widgets/WidgetMessagingStore.ts | 107 +-----
 src/utils/WidgetUtils.js                   |   1 -
 src/widgets/WidgetApi.ts                   |   1 -
 7 files changed, 273 insertions(+), 434 deletions(-)
 create mode 100644 src/stores/widgets/StopGapWidget.ts
 create mode 100644 src/stores/widgets/StopGapWidgetDriver.ts

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 = <span>&nbsp;-&nbsp;</span>;
         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 {
                             <iframe
                                 allow={iframeFeatures}
                                 ref={this._appFrame}
-                                src={this._getRenderedUrl()}
+                                src={this._sgWidget.embedUrl}
                                 allowFullScreen={true}
                                 sandbox={sandboxFlags}
                                 onLoad={this._onLoaded} />
@@ -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<IState> {
     /**
      * 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<void> {
+        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<Capability>): Promise<Set<Capability>> {
+        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<unknown> {
     private static internalInstance = new WidgetMessagingStore();
 
-    // <room/user ID, <widget ID, Widget>>
-    private widgetMap = new EnhancedMap<string, EnhancedMap<string, WidgetSurrogate>>();
+    private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget ID, ClientWidgetAPi>
 
     public constructor() {
         super(defaultDispatcher);
@@ -51,106 +50,16 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
         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 {

From fc1cbc668c6473a43c5760d5060554a2a746b054 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 14:33:46 -0600
Subject: [PATCH 08/21] Get the widget loading again

---
 src/components/views/elements/AppTile.js | 32 +++++++++++++-----------
 1 file changed, 18 insertions(+), 14 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index f6f6d22991..8888fe79b4 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -54,7 +54,6 @@ export default class AppTile extends React.Component {
         this.state = this._getNewState(props);
 
         this._onAction = this._onAction.bind(this);
-        this._onLoaded = this._onLoaded.bind(this);
         this._onEditClick = this._onEditClick.bind(this);
         this._onDeleteClick = this._onDeleteClick.bind(this);
         this._onRevokeClicked = this._onRevokeClicked.bind(this);
@@ -67,7 +66,6 @@ export default class AppTile extends React.Component {
         this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
 
         this._contextMenuButton = createRef();
-        this._appFrame = createRef();
         this._menu_bar = createRef();
     }
 
@@ -90,7 +88,6 @@ export default class AppTile extends React.Component {
             initialising: true, // True while we are mangling the widget URL
             // True while the iframe content is loading
             loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
-            widgetUrl: this._addWurlParams(newProps.app.url),
             // Assume that widget has permission to load if we are the user who
             // added it to the room, or if explicitly granted by the user
             hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
@@ -149,11 +146,18 @@ export default class AppTile extends React.Component {
 
     _startWidget() {
         this._sgWidget.prepare().then(() => {
-            if (this._appFrame.current) {
-                this._sgWidget.start(this._appFrame.current);
-            }
+            this.setState({initialising: false});
         });
     }
+    
+    _iframeRefChange = (ref) => {
+        this.setState({iframe: ref});
+        if (ref) {
+            this._sgWidget.start(ref);
+        } else {
+            this._resetWidget(this.props);
+        }
+    };
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
     UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
@@ -222,14 +226,14 @@ export default class AppTile extends React.Component {
         // 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) {
+        if (this.state.iframe) {
             // 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';
+            this.state.iframe.src = 'about:blank';
         }
 
         // Delete the widget from the persisted store for good measure.
@@ -420,9 +424,9 @@ export default class AppTile extends React.Component {
         // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
         if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
             this._endWidgetActions().then(() => {
-                if (this._appFrame.current) {
+                if (this.state.iframe) {
                     // Reload iframe
-                    this._appFrame.current.src = this._sgWidget.embedUrl;
+                    this.state.iframe.src = this._sgWidget.embedUrl;
                     this.setState({});
                 }
             });
@@ -436,7 +440,7 @@ export default class AppTile extends React.Component {
     _onReloadWidgetClick() {
         // Reload iframe in this way to avoid cross-origin restrictions
         // eslint-disable-next-line no-self-assign
-        this._appFrame.current.src = this._appFrame.current.src;
+        this.state.iframe.src = this.state.iframe.src;
     }
 
     _onContextMenuClick = () => {
@@ -482,7 +486,7 @@ export default class AppTile extends React.Component {
                         <AppPermission
                             roomId={this.props.room.roomId}
                             creatorUserId={this.props.creatorUserId}
-                            url={this.state.widgetUrl}
+                            url={this._sgWidget.embedUrl}
                             isRoomEncrypted={isEncrypted}
                             onPermissionGranted={this._grantWidgetPermission}
                         />
@@ -507,11 +511,11 @@ export default class AppTile extends React.Component {
                             { this.state.loading && loadingElement }
                             <iframe
                                 allow={iframeFeatures}
-                                ref={this._appFrame}
+                                ref={this._iframeRefChange}
                                 src={this._sgWidget.embedUrl}
                                 allowFullScreen={true}
                                 sandbox={sandboxFlags}
-                                onLoad={this._onLoaded} />
+                            />
                         </div>
                     );
                     // if the widget would be allowed to remain on screen, we must put it in

From ca76ba5cf1d14acd604a8163b5fc2c7d687678c8 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 15:09:52 -0600
Subject: [PATCH 09/21] Fix widget persistence

---
 src/stores/widgets/StopGapWidget.ts | 36 +++++++++++++++++++++++++++--
 1 file changed, 34 insertions(+), 2 deletions(-)

diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 2b8ab9f5a8..4b6ce70a6e 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -15,7 +15,14 @@
  */
 
 import {Room} from "matrix-js-sdk/src/models/room";
-import { ClientWidgetApi, IWidget, IWidgetData, Widget } from "matrix-widget-api";
+import {
+    ClientWidgetApi,
+    IStickyActionRequest,
+    IWidget,
+    IWidgetApiRequestEmptyData,
+    IWidgetData,
+    Widget
+} from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";
 import { WidgetMessagingStore } from "./WidgetMessagingStore";
@@ -26,6 +33,8 @@ import WidgetUtils from '../../utils/WidgetUtils';
 import { IntegrationManagers } from "../../integrations/IntegrationManagers";
 import SettingsStore from "../../settings/SettingsStore";
 import { WidgetType } from "../../widgets/WidgetType";
+import { Capability } from "../../widgets/WidgetApi";
+import ActiveWidgetStore from "../ActiveWidgetStore";
 
 // TODO: Destroy all of this code
 
@@ -138,14 +147,32 @@ export class StopGapWidget extends EventEmitter {
 
     public start(iframe: HTMLIFrameElement) {
         if (this.started) return;
-        const driver = new StopGapWidgetDriver(this.appTileProps.whitelistCapabilities || []);
+        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);
+
+        if (!this.appTileProps.userWidget && this.appTileProps.room) {
+            ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
+        }
+
+        if (WidgetType.JITSI.matches(this.mockWidget.type)) {
+            this.messaging.addEventListener("action:set_always_on_screen",
+                (ev: CustomEvent<IStickyActionRequest>) => {
+                    if (this.messaging.hasCapability(Capability.AlwaysOnScreen)) {
+                        ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
+                        ev.preventDefault();
+                        this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
+                    }
+                },
+            );
+        }
     }
 
     public async prepare(): Promise<void> {
         if (this.scalarToken) return;
+        const existingMessaging = WidgetMessagingStore.instance.getMessaging(this.mockWidget);
+        if (existingMessaging) this.messaging = existingMessaging;
         try {
             if (WidgetUtils.isScalarUrl(this.mockWidget.templateUrl)) {
                 const managers = IntegrationManagers.sharedInstance();
@@ -165,7 +192,12 @@ export class StopGapWidget extends EventEmitter {
     }
 
     public stop() {
+        if (ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
+            console.log("Skipping destroy - persistent widget");
+            return;
+        }
         if (!this.started) return;
         WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
+        ActiveWidgetStore.delRoomId(this.mockWidget.id);
     }
 }

From bb5184bc50a0dfd63c73bbcb718f6b837bc98eb1 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 15:32:07 -0600
Subject: [PATCH 10/21] Remove unused function

---
 src/utils/WidgetUtils.js | 12 ------------
 1 file changed, 12 deletions(-)

diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index 57459ba897..9373738bf8 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -494,16 +494,4 @@ export default class WidgetUtils {
             IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
         }
     }
-
-    static snapshotWidget(app) {
-        console.log("Requesting widget snapshot");
-        ActiveWidgetStore.getWidgetMessaging(app.id).getScreenshot().catch((err) => {
-            console.error("Failed to get screenshot", err);
-        }).then((screenshot) => {
-            dis.dispatch({
-                action: 'picture_snapshot',
-                file: screenshot,
-            }, true);
-        });
-    }
 }

From 555bcc6010c19b019a96fec08db4dbd4346fc44b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 15:32:18 -0600
Subject: [PATCH 11/21] Document remaining (known) cases to fix

---
 src/CallHandler.tsx                             | 1 +
 src/components/views/right_panel/WidgetCard.tsx | 1 +
 src/components/views/rooms/Stickerpicker.js     | 1 +
 3 files changed, 3 insertions(+)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 2ff018d4d6..849859eb20 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -495,6 +495,7 @@ export default class CallHandler {
         const roomInfo = WidgetStore.instance.getRoom(roomId);
         if (!roomInfo) return; // "should never happen" clauses go here
 
+        // TODO: [TravisR] Fix this
         const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
         jitsiWidgets.forEach(w => {
             const messaging = ActiveWidgetStore.getWidgetMessaging(w.id);
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index 1677494708..b0eefb0fed 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -77,6 +77,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
     let contextMenu;
     if (menuDisplayed) {
         let snapshotButton;
+        // TODO: [TravisR] Fix this
         if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) {
             const onSnapshotClick = () => {
                 WidgetUtils.snapshotWidget(app);
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index dba25a94cf..548e1d02bb 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -212,6 +212,7 @@ export default class Stickerpicker extends React.Component {
 
     _sendVisibilityToWidget(visible) {
         if (!this.state.stickerpickerWidget) return;
+        // TODO: [TravisR] Fix this
         const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
         if (widgetMessaging && visible !== this._prevSentVisibility) {
             widgetMessaging.sendVisibility(visible);

From 9190c921d2d47bd2fa234be620fd788f79b1e2b0 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 15:35:04 -0600
Subject: [PATCH 12/21] Clean up failed experiment

---
 src/stores/widgets/SdkWidgetDriver.ts      | 34 ----------------------
 src/stores/widgets/WidgetMessagingStore.ts |  5 +---
 src/stores/widgets/WidgetSurrogate.ts      | 25 ----------------
 3 files changed, 1 insertion(+), 63 deletions(-)
 delete mode 100644 src/stores/widgets/SdkWidgetDriver.ts
 delete mode 100644 src/stores/widgets/WidgetSurrogate.ts

diff --git a/src/stores/widgets/SdkWidgetDriver.ts b/src/stores/widgets/SdkWidgetDriver.ts
deleted file mode 100644
index 1462303fa3..0000000000
--- a/src/stores/widgets/SdkWidgetDriver.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api";
-import { iterableUnion } from "../../utils/iterables";
-
-export class SdkWidgetDriver extends WidgetDriver {
-    public constructor(
-        private widget: Widget,
-        private widgetKind: WidgetKind,
-        private locationEntityId: string,
-        private preapprovedCapabilities: Set<Capability> = new Set(),
-    ) {
-        super();
-    }
-
-    public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
-        // TODO: Prompt the user to accept capabilities
-        return iterableUnion(requested, this.preapprovedCapabilities);
-    }
-}
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index fa743fdeaf..34fc2c4e04 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -14,13 +14,10 @@
  * limitations under the License.
  */
 
-import { ClientWidgetApi, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api";
+import { ClientWidgetApi, Widget } from "matrix-widget-api";
 import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
 import defaultDispatcher from "../../dispatcher/dispatcher";
 import { ActionPayload } from "../../dispatcher/payloads";
-import { Room } from "matrix-js-sdk/src/models/room";
-import { WidgetSurrogate } from "./WidgetSurrogate";
-import { SdkWidgetDriver } from "./SdkWidgetDriver";
 import { EnhancedMap } from "../../utils/maps";
 
 /**
diff --git a/src/stores/widgets/WidgetSurrogate.ts b/src/stores/widgets/WidgetSurrogate.ts
deleted file mode 100644
index 4d482124a6..0000000000
--- a/src/stores/widgets/WidgetSurrogate.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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 { ClientWidgetApi, Widget } from "matrix-widget-api";
-
-export class WidgetSurrogate {
-    public constructor(
-        public readonly definition: Widget,
-        public readonly messaging: ClientWidgetApi,
-    ) {
-    }
-}

From b710d42832579c3d1543f42e4bbf0307e610a4f4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 16:12:00 -0600
Subject: [PATCH 13/21] Fix stickerpicker to use new messaging

---
 src/components/views/rooms/Stickerpicker.js |  9 ++-
 src/stores/widgets/StopGapWidget.ts         | 68 +++++++++++++++++++--
 src/stores/widgets/WidgetMessagingStore.ts  | 11 ++++
 3 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 548e1d02bb..039d2571f4 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -30,6 +30,7 @@ import {ContextMenu} from "../../structures/ContextMenu";
 import {WidgetType} from "../../../widgets/WidgetType";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import {Action} from "../../../dispatcher/actions";
+import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
 
 // This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
 // We sit in a context menu, so this should be given to the context menu.
@@ -213,9 +214,11 @@ export default class Stickerpicker extends React.Component {
     _sendVisibilityToWidget(visible) {
         if (!this.state.stickerpickerWidget) return;
         // TODO: [TravisR] Fix this
-        const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
-        if (widgetMessaging && visible !== this._prevSentVisibility) {
-            widgetMessaging.sendVisibility(visible);
+        const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id);
+        if (messaging && visible !== this._prevSentVisibility) {
+            messaging.updateVisibility(visible).catch(err => {
+                console.error("Error updating widget visibility: ", err);
+            });
             this._prevSentVisibility = visible;
         }
     }
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 4b6ce70a6e..073073abec 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -16,12 +16,12 @@
 
 import {Room} from "matrix-js-sdk/src/models/room";
 import {
-    ClientWidgetApi,
+    ClientWidgetApi, IStickerActionRequest,
     IStickyActionRequest,
-    IWidget,
+    IWidget, IWidgetApiRequest,
     IWidgetApiRequestEmptyData,
     IWidgetData,
-    Widget
+    Widget, WidgetApiFromWidgetAction
 } from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";
@@ -35,6 +35,9 @@ import SettingsStore from "../../settings/SettingsStore";
 import { WidgetType } from "../../widgets/WidgetType";
 import { Capability } from "../../widgets/WidgetApi";
 import ActiveWidgetStore from "../ActiveWidgetStore";
+import { objectShallowClone } from "../../utils/objects";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import dis from "../../dispatcher/dispatcher";
 
 // TODO: Destroy all of this code
 
@@ -88,7 +91,15 @@ export class StopGapWidget extends EventEmitter {
 
     constructor(private appTileProps: IAppTileProps) {
         super();
-        this.mockWidget = new ElementWidget(appTileProps.app);
+        let app = appTileProps.app;
+
+        // Backwards compatibility: not all old widgets have a creatorUserId
+        if (!app.creatorUserId) {
+            app = objectShallowClone(app); // clone to prevent accidental mutation
+            app.creatorUserId = MatrixClientPeg.get().getUserId();
+        }
+
+        this.mockWidget = new ElementWidget(app);
     }
 
     public get widgetApi(): ClientWidgetApi {
@@ -166,6 +177,55 @@ export class StopGapWidget extends EventEmitter {
                     }
                 },
             );
+        } else if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
+            this.messaging.addEventListener("action:integration_manager_open",
+                (ev: CustomEvent<IWidgetApiRequest>) => {
+                    // Acknowledge first
+                    ev.preventDefault();
+                    this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
+
+                    // First close the stickerpicker
+                    defaultDispatcher.dispatch({action: "stickerpicker_close"});
+
+                    // Now open the integration manager
+                    // TODO: Spec this interaction.
+                    const data = ev.detail.data;
+                    const integType = data?.integType
+                    const integId = <string>data?.integId;
+
+                    // TODO: Open the right integration manager for the widget
+                    if (SettingsStore.getValue("feature_many_integration_managers")) {
+                        IntegrationManagers.sharedInstance().openAll(
+                            MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
+                            `type_${integType}`,
+                            integId,
+                        );
+                    } else {
+                        IntegrationManagers.sharedInstance().getPrimaryManager().open(
+                            MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
+                            `type_${integType}`,
+                            integId,
+                        );
+                    }
+                },
+            );
+
+            // TODO: Replace this event listener with appropriate driver functionality once the API
+            // establishes a sane way to send events back and forth.
+            this.messaging.addEventListener(`action:${WidgetApiFromWidgetAction.SendSticker}`,
+                (ev: CustomEvent<IStickerActionRequest>) => {
+                    // Acknowledge first
+                    ev.preventDefault();
+                    this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
+
+                    // Send the sticker
+                    defaultDispatcher.dispatch({
+                        action: 'm.sticker',
+                        data: ev.detail.data,
+                        widgetId: this.mockWidget.id,
+                    });
+                },
+            );
         }
     }
 
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index 34fc2c4e04..828465ce84 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -28,6 +28,7 @@ import { EnhancedMap } from "../../utils/maps";
 export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
     private static internalInstance = new WidgetMessagingStore();
 
+    // TODO: Fix uniqueness problem (widget IDs are not unique across the whole app)
     private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget ID, ClientWidgetAPi>
 
     public constructor() {
@@ -59,4 +60,14 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
     public getMessaging(widget: Widget): ClientWidgetApi {
         return this.widgetMap.get(widget.id);
     }
+
+    /**
+     * Gets the widget messaging class for a given widget ID.
+     * @param {string} widgetId The widget ID.
+     * @returns {ClientWidgetApi} The widget API, or a falsey value if not found.
+     * @deprecated Widget IDs are not globally unique.
+     */
+    public getMessagingForId(widgetId: string): ClientWidgetApi {
+        return this.widgetMap.get(widgetId);
+    }
 }

From 9b984a35e063f51fac8658199a7afd37f93c06fc Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 19:58:16 -0600
Subject: [PATCH 14/21] Remove dead AppTile2

---
 src/components/views/elements/AppTile2.tsx | 77 ----------------------
 1 file changed, 77 deletions(-)
 delete mode 100644 src/components/views/elements/AppTile2.tsx

diff --git a/src/components/views/elements/AppTile2.tsx b/src/components/views/elements/AppTile2.tsx
deleted file mode 100644
index 516c00170a..0000000000
--- a/src/components/views/elements/AppTile2.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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 { ClientWidgetApi, Widget, WidgetKind } from "matrix-widget-api";
-import * as React from "react";
-import { Room } from "matrix-js-sdk/src/models/room";
-import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
-
-interface IProps {
-    widget: Widget;
-    kind: WidgetKind;
-    room?: Room;
-
-    // TODO: All the showUIElement props
-}
-
-interface IState {
-    loading: boolean;
-}
-
-export default class AppTile2 extends React.PureComponent<IProps, IState> {
-    private messaging: ClientWidgetApi;
-    private iframeRef = React.createRef<HTMLIFrameElement>();
-
-    public constructor(props: IProps) {
-        super(props);
-
-        if (props.kind === WidgetKind.Room && !props.room) {
-            throw new Error("Expected room when supplied with a room widget");
-        }
-
-        this.state = {
-            loading: true,
-        };
-    }
-
-    private get isMixedContent(): boolean {
-        const myProtocol = window.location.protocol;
-        const widgetProtocol = new URL(this.props.widget.templateUrl).protocol;
-        return myProtocol === 'https:' && widgetProtocol !== 'https:';
-    }
-
-    public componentDidMount() {
-        if (!this.iframeRef.current) {
-            throw new Error("iframe has not yet been associated - fix the render code");
-        }
-
-        // TODO: Provide capabilities to widget messaging
-
-        if (this.props.kind === WidgetKind.Room) {
-            this.messaging = WidgetMessagingStore.instance
-                .generateMessagingForRoomWidget(this.props.room, this.props.widget, this.iframeRef.current);
-        } else if (this.props.kind === WidgetKind.Account) {
-            this.messaging = WidgetMessagingStore.instance
-                .generateMessagingForAccountWidget(this.props.widget, this.iframeRef.current);
-        } else {
-            throw new Error("Unexpected widget kind: " + this.props.kind);
-        }
-
-        this.messaging.once("ready", () => {
-            this.setState({loading: false});
-        });
-    }
-}

From b46f58274e2b097a2067ee1bb12c2b86d362f481 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:09:23 -0600
Subject: [PATCH 15/21] Fix the hangup button and other custom actions

---
 src/CallHandler.tsx                         |  7 ++++---
 src/components/views/elements/AppTile.js    |  3 ++-
 src/components/views/rooms/Stickerpicker.js |  1 -
 src/stores/widgets/ElementWidgetActions.ts  | 21 +++++++++++++++++++++
 src/stores/widgets/StopGapWidget.ts         |  3 ++-
 5 files changed, 29 insertions(+), 6 deletions(-)
 create mode 100644 src/stores/widgets/ElementWidgetActions.ts

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 849859eb20..49f82e3209 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -76,6 +76,8 @@ import QuestionDialog from "./components/views/dialogs/QuestionDialog";
 import ErrorDialog from "./components/views/dialogs/ErrorDialog";
 import WidgetStore from "./stores/WidgetStore";
 import ActiveWidgetStore from "./stores/ActiveWidgetStore";
+import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
+import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
 
 // until we ts-ify the js-sdk voip code
 type Call = any;
@@ -495,13 +497,12 @@ export default class CallHandler {
         const roomInfo = WidgetStore.instance.getRoom(roomId);
         if (!roomInfo) return; // "should never happen" clauses go here
 
-        // TODO: [TravisR] Fix this
         const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
         jitsiWidgets.forEach(w => {
-            const messaging = ActiveWidgetStore.getWidgetMessaging(w.id);
+            const messaging = WidgetMessagingStore.instance.getMessagingForId(w.id);
             if (!messaging) return; // more "should never happen" words
 
-            messaging.hangup();
+            messaging.transport.send(ElementWidgetActions.HangupCall, {});
         });
     }
 }
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 8888fe79b4..e8ef4de257 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -41,6 +41,7 @@ import {SettingLevel} from "../../../settings/SettingLevel";
 import WidgetStore from "../../../stores/WidgetStore";
 import {Action} from "../../../dispatcher/actions";
 import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
+import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
 
 export default class AppTile extends React.Component {
     constructor(props) {
@@ -296,7 +297,7 @@ export default class AppTile extends React.Component {
     _onWidgetReady = () => {
         this.setState({loading: false});
         if (WidgetType.JITSI.matches(this.props.app.type)) {
-            this._sgWidget.widgetApi.transport.send("im.vector.ready", {});
+            this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
         }
     };
 
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 039d2571f4..d191e05407 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -213,7 +213,6 @@ export default class Stickerpicker extends React.Component {
 
     _sendVisibilityToWidget(visible) {
         if (!this.state.stickerpickerWidget) return;
-        // TODO: [TravisR] Fix this
         const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id);
         if (messaging && visible !== this._prevSentVisibility) {
             messaging.updateVisibility(visible).catch(err => {
diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts
new file mode 100644
index 0000000000..b101a119a4
--- /dev/null
+++ b/src/stores/widgets/ElementWidgetActions.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 enum ElementWidgetActions {
+    ClientReady = "im.vector.ready",
+    HangupCall = "im.vector.hangup",
+    OpenIntegrationManager = "integration_manager_open",
+}
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 073073abec..cd66522488 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -38,6 +38,7 @@ import ActiveWidgetStore from "../ActiveWidgetStore";
 import { objectShallowClone } from "../../utils/objects";
 import defaultDispatcher from "../../dispatcher/dispatcher";
 import dis from "../../dispatcher/dispatcher";
+import { ElementWidgetActions } from "./ElementWidgetActions";
 
 // TODO: Destroy all of this code
 
@@ -178,7 +179,7 @@ export class StopGapWidget extends EventEmitter {
                 },
             );
         } else if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
-            this.messaging.addEventListener("action:integration_manager_open",
+            this.messaging.addEventListener(`action:${ElementWidgetActions.OpenIntegrationManager}`,
                 (ev: CustomEvent<IWidgetApiRequest>) => {
                     // Acknowledge first
                     ev.preventDefault();

From 9377306b813bdbbeb7bee6e6a52dad6945fea445 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:11:27 -0600
Subject: [PATCH 16/21] Fix the screenshot button on the right panel card

---
 src/components/views/right_panel/WidgetCard.tsx | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index b0eefb0fed..6bb45df109 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -39,6 +39,8 @@ import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPa
 import {Capability} from "../../../widgets/WidgetApi";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import classNames from "classnames";
+import dis from "../../../dispatcher/dispatcher";
+import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
 
 interface IProps {
     room: Room;
@@ -77,10 +79,17 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
     let contextMenu;
     if (menuDisplayed) {
         let snapshotButton;
-        // TODO: [TravisR] Fix this
-        if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) {
+        const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
+        if (widgetMessaging?.hasCapability(Capability.Screenshot)) {
             const onSnapshotClick = () => {
-                WidgetUtils.snapshotWidget(app);
+                widgetMessaging.takeScreenshot().then(data => {
+                    dis.dispatch({
+                        action: 'picture_snapshot',
+                        file: data.screenshot,
+                    });
+                }).catch(err => {
+                    console.error("Failed to take screenshot: ", err);
+                });
                 closeMenu();
             };
 

From f27071ee64db80ddbe551a4d6eac7ac1b00ea5a4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:20:31 -0600
Subject: [PATCH 17/21] Transition all remaining messaging over (delete the old
 stuff)

---
 src/CallHandler.tsx                           |   1 -
 src/FromWidgetPostMessageApi.js               | 278 ------------------
 src/ToWidgetPostMessageApi.js                 |  84 ------
 src/WidgetMessaging.js                        | 223 --------------
 src/WidgetMessagingEndpoint.js                |  37 ---
 src/components/views/elements/AppTile.js      |   6 +-
 .../views/right_panel/WidgetCard.tsx          |   5 +-
 src/stores/ActiveWidgetStore.js               |   9 +-
 src/stores/widgets/StopGapWidget.ts           |  16 +-
 src/stores/widgets/WidgetMessagingStore.ts    |   9 +
 src/utils/WidgetUtils.js                      |   6 +-
 src/widgets/WidgetApi.ts                      | 222 --------------
 src/widgets/WidgetType.ts                     |   1 +
 13 files changed, 29 insertions(+), 868 deletions(-)
 delete mode 100644 src/FromWidgetPostMessageApi.js
 delete mode 100644 src/ToWidgetPostMessageApi.js
 delete mode 100644 src/WidgetMessaging.js
 delete mode 100644 src/WidgetMessagingEndpoint.js
 delete mode 100644 src/widgets/WidgetApi.ts

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 49f82e3209..2259913c6d 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -75,7 +75,6 @@ import {base32} from "rfc4648";
 import QuestionDialog from "./components/views/dialogs/QuestionDialog";
 import ErrorDialog from "./components/views/dialogs/ErrorDialog";
 import WidgetStore from "./stores/WidgetStore";
-import ActiveWidgetStore from "./stores/ActiveWidgetStore";
 import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
 import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
 
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
deleted file mode 100644
index bbccc47d28..0000000000
--- a/src/FromWidgetPostMessageApi.js
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 Travis Ralston
-Copyright 2019 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 URL from 'url';
-import dis from './dispatcher/dispatcher';
-import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
-import ActiveWidgetStore from './stores/ActiveWidgetStore';
-import {MatrixClientPeg} from "./MatrixClientPeg";
-import RoomViewStore from "./stores/RoomViewStore";
-import {IntegrationManagers} from "./integrations/IntegrationManagers";
-import SettingsStore from "./settings/SettingsStore";
-import {Capability} from "./widgets/WidgetApi";
-import {objectClone} from "./utils/objects";
-
-const WIDGET_API_VERSION = '0.0.2'; // Current API version
-const SUPPORTED_WIDGET_API_VERSIONS = [
-    '0.0.1',
-    '0.0.2',
-];
-const INBOUND_API_NAME = 'fromWidget';
-
-// Listen for and handle incoming requests using the 'fromWidget' postMessage
-// API and initiate responses
-export default class FromWidgetPostMessageApi {
-    constructor() {
-        this.widgetMessagingEndpoints = [];
-        this.widgetListeners = {}; // {action: func[]}
-
-        this.start = this.start.bind(this);
-        this.stop = this.stop.bind(this);
-        this.onPostMessage = this.onPostMessage.bind(this);
-    }
-
-    start() {
-        window.addEventListener('message', this.onPostMessage);
-    }
-
-    stop() {
-        window.removeEventListener('message', this.onPostMessage);
-    }
-
-    /**
-     * Adds a listener for a given action
-     * @param {string} action The action to listen for.
-     * @param {Function} callbackFn A callback function to be called when the action is
-     * encountered. Called with two parameters: the interesting request information and
-     * the raw event received from the postMessage API. The raw event is meant to be used
-     * for sendResponse and similar functions.
-     */
-    addListener(action, callbackFn) {
-        if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
-        this.widgetListeners[action].push(callbackFn);
-    }
-
-    /**
-     * Removes a listener for a given action.
-     * @param {string} action The action that was subscribed to.
-     * @param {Function} callbackFn The original callback function that was used to subscribe
-     * to updates.
-     */
-    removeListener(action, callbackFn) {
-        if (!this.widgetListeners[action]) return;
-
-        const idx = this.widgetListeners[action].indexOf(callbackFn);
-        if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
-    }
-
-    /**
-     * Register a widget endpoint for trusted postMessage communication
-     * @param {string} widgetId    Unique widget identifier
-     * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
-     */
-    addEndpoint(widgetId, endpointUrl) {
-        const u = URL.parse(endpointUrl);
-        if (!u || !u.protocol || !u.host) {
-            console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
-            return;
-        }
-
-        const origin = u.protocol + '//' + u.host;
-        const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
-        if (this.widgetMessagingEndpoints.some(function(ep) {
-            return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
-        })) {
-            // Message endpoint already registered
-            console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
-            return;
-        } else {
-            console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
-            this.widgetMessagingEndpoints.push(endpoint);
-        }
-    }
-
-    /**
-     * De-register a widget endpoint from trusted communication sources
-     * @param  {string} widgetId Unique widget identifier
-     * @param  {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
-     * @return {boolean} True if endpoint was successfully removed
-     */
-    removeEndpoint(widgetId, endpointUrl) {
-        const u = URL.parse(endpointUrl);
-        if (!u || !u.protocol || !u.host) {
-            console.warn('Remove widget messaging endpoint - Invalid origin');
-            return;
-        }
-
-        const origin = u.protocol + '//' + u.host;
-        if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
-            const length = this.widgetMessagingEndpoints.length;
-            this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
-                .filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
-            return (length > this.widgetMessagingEndpoints.length);
-        }
-        return false;
-    }
-
-    /**
-     * Handle widget postMessage events
-     * Messages are only handled where a valid, registered messaging endpoints
-     * @param  {Event} event Event to handle
-     * @return {undefined}
-     */
-    onPostMessage(event) {
-        if (!event.origin) { // Handle chrome
-            event.origin = event.originalEvent.origin;
-        }
-
-        // Event origin is empty string if undefined
-        if (
-            event.origin.length === 0 ||
-            !this.trustedEndpoint(event.origin) ||
-            event.data.api !== INBOUND_API_NAME ||
-            !event.data.widgetId
-        ) {
-            return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
-        }
-
-        // Call any listeners we have registered
-        if (this.widgetListeners[event.data.action]) {
-            for (const fn of this.widgetListeners[event.data.action]) {
-                fn(event.data, event);
-            }
-        }
-
-        // Although the requestId is required, we don't use it. We'll be nice and process the message
-        // if the property is missing, but with a warning for widget developers.
-        if (!event.data.requestId) {
-            console.warn("fromWidget action '" + event.data.action + "' does not have a requestId");
-        }
-
-        const action = event.data.action;
-        const widgetId = event.data.widgetId;
-        if (action === 'content_loaded') {
-            console.log('Widget reported content loaded for', widgetId);
-            dis.dispatch({
-                action: 'widget_content_loaded',
-                widgetId: widgetId,
-            });
-            this.sendResponse(event, {success: true});
-        } else if (action === 'supported_api_versions') {
-            this.sendResponse(event, {
-                api: INBOUND_API_NAME,
-                supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
-            });
-        } else if (action === 'api_version') {
-            this.sendResponse(event, {
-                api: INBOUND_API_NAME,
-                version: WIDGET_API_VERSION,
-            });
-        } else if (action === 'm.sticker') {
-            // console.warn('Got sticker message from widget', widgetId);
-            // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
-            const data = event.data.data || event.data.widgetData;
-            dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId});
-        } else if (action === 'integration_manager_open') {
-            // Close the stickerpicker
-            dis.dispatch({action: 'stickerpicker_close'});
-            // Open the integration manager
-            // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
-            const data = event.data.data || event.data.widgetData;
-            const integType = (data && data.integType) ? data.integType : null;
-            const integId = (data && data.integId) ? data.integId : null;
-
-            // TODO: Open the right integration manager for the widget
-            if (SettingsStore.getValue("feature_many_integration_managers")) {
-                IntegrationManagers.sharedInstance().openAll(
-                    MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
-                    `type_${integType}`,
-                    integId,
-                );
-            } else {
-                IntegrationManagers.sharedInstance().getPrimaryManager().open(
-                    MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
-                    `type_${integType}`,
-                    integId,
-                );
-            }
-        } else if (action === 'set_always_on_screen') {
-            // This is a new message: there is no reason to support the deprecated widgetData here
-            const data = event.data.data;
-            const val = data.value;
-
-            if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
-                ActiveWidgetStore.setWidgetPersistence(widgetId, val);
-            }
-
-            // acknowledge
-            this.sendResponse(event, {});
-        } else if (action === 'get_openid') {
-            // Handled by caller
-        } else {
-            console.warn('Widget postMessage event unhandled');
-            this.sendError(event, {message: 'The postMessage was unhandled'});
-        }
-    }
-
-    /**
-     * Check if message origin is registered as trusted
-     * @param  {string} origin PostMessage origin to check
-     * @return {boolean}       True if trusted
-     */
-    trustedEndpoint(origin) {
-        if (!origin) {
-            return false;
-        }
-
-        return this.widgetMessagingEndpoints.some((endpoint) => {
-            // TODO / FIXME -- Should this also check the widgetId?
-            return endpoint.endpointUrl === origin;
-        });
-    }
-
-    /**
-     * Send a postmessage response to a postMessage request
-     * @param  {Event} event  The original postMessage request event
-     * @param  {Object} res   Response data
-     */
-    sendResponse(event, res) {
-        const data = objectClone(event.data);
-        data.response = res;
-        event.source.postMessage(data, event.origin);
-    }
-
-    /**
-     * Send an error response to a postMessage request
-     * @param  {Event} event        The original postMessage request event
-     * @param  {string} msg         Error message
-     * @param  {Error} nestedError  Nested error event (optional)
-     */
-    sendError(event, msg, nestedError) {
-        console.error('Action:' + event.data.action + ' failed with message: ' + msg);
-        const data = objectClone(event.data);
-        data.response = {
-            error: {
-                message: msg,
-            },
-        };
-        if (nestedError) {
-            data.response.error._error = nestedError;
-        }
-        event.source.postMessage(data, event.origin);
-    }
-}
diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js
deleted file mode 100644
index 00309d252c..0000000000
--- a/src/ToWidgetPostMessageApi.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
-Copyright 2018 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.
-*/
-
-// const OUTBOUND_API_NAME = 'toWidget';
-
-// Initiate requests using the "toWidget" postMessage API and handle responses
-// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
-// response field
-export default class ToWidgetPostMessageApi {
-    constructor(timeoutMs) {
-        this._timeoutMs = timeoutMs || 5000; // default to 5s timer
-        this._counter = 0;
-        this._requestMap = {
-            // $ID: {resolve, reject}
-        };
-        this.start = this.start.bind(this);
-        this.stop = this.stop.bind(this);
-        this.onPostMessage = this.onPostMessage.bind(this);
-    }
-
-    start() {
-        window.addEventListener('message', this.onPostMessage);
-    }
-
-    stop() {
-        window.removeEventListener('message', this.onPostMessage);
-    }
-
-    onPostMessage(ev) {
-        // THIS IS ALL UNSAFE EXECUTION.
-        // We do not verify who the sender of `ev` is!
-        const payload = ev.data;
-        // NOTE: Workaround for running in a mobile WebView where a
-        // postMessage immediately triggers this callback even though it is
-        // not the response.
-        if (payload.response === undefined) {
-            return;
-        }
-        const promise = this._requestMap[payload.requestId];
-        if (!promise) {
-            return;
-        }
-        delete this._requestMap[payload.requestId];
-        promise.resolve(payload);
-    }
-
-    // Initiate outbound requests (toWidget)
-    exec(action, targetWindow, targetOrigin) {
-        targetWindow = targetWindow || window.parent; // default to parent window
-        targetOrigin = targetOrigin || "*";
-        this._counter += 1;
-        action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
-
-        return new Promise((resolve, reject) => {
-            this._requestMap[action.requestId] = {resolve, reject};
-            targetWindow.postMessage(action, targetOrigin);
-
-            if (this._timeoutMs > 0) {
-                setTimeout(() => {
-                    if (!this._requestMap[action.requestId]) {
-                        return;
-                    }
-                    console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
-                        this._requestMap);
-                    this._requestMap[action.requestId].reject(new Error("Timed out"));
-                    delete this._requestMap[action.requestId];
-                }, this._timeoutMs);
-            }
-        });
-    }
-}
diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
deleted file mode 100644
index 9394abf025..0000000000
--- a/src/WidgetMessaging.js
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
-Copyright 2017 New Vector Ltd
-Copyright 2019 Travis Ralston
-
-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.
-*/
-
-/*
-* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
-* spec. details / documentation.
-*/
-
-import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
-import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
-import Modal from "./Modal";
-import {MatrixClientPeg} from "./MatrixClientPeg";
-import SettingsStore from "./settings/SettingsStore";
-import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
-import WidgetUtils from "./utils/WidgetUtils";
-import {KnownWidgetActions} from "./widgets/WidgetApi";
-
-if (!global.mxFromWidgetMessaging) {
-    global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
-    global.mxFromWidgetMessaging.start();
-}
-if (!global.mxToWidgetMessaging) {
-    global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
-    global.mxToWidgetMessaging.start();
-}
-
-const OUTBOUND_API_NAME = 'toWidget';
-
-export default class WidgetMessaging {
-    /**
-     * @param {string} widgetId The widget's ID
-     * @param {string} wurl The raw URL of the widget as in the event (the 'wURL')
-     * @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL
-     *     or a different URL of the clients choosing if it is using its own impl).
-     * @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget
-     * @param {object} target Where widget messages should be sent (eg. the iframe object)
-     */
-    constructor(widgetId, wurl, renderedUrl, isUserWidget, target) {
-        this.widgetId = widgetId;
-        this.wurl = wurl;
-        this.renderedUrl = renderedUrl;
-        this.isUserWidget = isUserWidget;
-        this.target = target;
-        this.fromWidget = global.mxFromWidgetMessaging;
-        this.toWidget = global.mxToWidgetMessaging;
-        this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
-        this.start();
-    }
-
-    messageToWidget(action) {
-        action.widgetId = this.widgetId; // Required to be sent for all outbound requests
-
-        return this.toWidget.exec(action, this.target).then((data) => {
-            // Check for errors and reject if found
-            if (data.response === undefined) { // null is valid
-                throw new Error("Missing 'response' field");
-            }
-            if (data.response && data.response.error) {
-                const err = data.response.error;
-                const msg = String(err.message ? err.message : "An error was returned");
-                if (err._error) {
-                    console.error(err._error);
-                }
-                // Potential XSS attack if 'msg' is not appropriately sanitized,
-                // as it is untrusted input by our parent window (which we assume is Element).
-                // We can't aggressively sanitize [A-z0-9] since it might be a translation.
-                throw new Error(msg);
-            }
-            // Return the response field for the request
-            return data.response;
-        });
-    }
-
-    /**
-     * Tells the widget that the client is ready to handle further widget requests.
-     * @returns {Promise<*>} Resolves after the widget has acknowledged the ready message.
-     */
-    flagReadyToContinue() {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.ClientReady,
-        });
-    }
-
-    /**
-     * Tells the widget that it should terminate now.
-     * @returns {Promise<*>} Resolves when widget has acknowledged the message.
-     */
-    terminate() {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.Terminate,
-        });
-    }
-
-    /**
-     * Tells the widget to hang up on its call.
-     * @returns {Promise<*>} Resolves when the widget has acknowledged the message.
-     */
-    hangup() {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.Hangup,
-        });
-    }
-
-    /**
-     * Request a screenshot from a widget
-     * @return {Promise} To be resolved with screenshot data when it has been generated
-     */
-    getScreenshot() {
-        console.log('Requesting screenshot for', this.widgetId);
-        return this.messageToWidget({
-                api: OUTBOUND_API_NAME,
-                action: "screenshot",
-            })
-            .catch((error) => new Error("Failed to get screenshot: " + error.message))
-            .then((response) => response.screenshot);
-    }
-
-    /**
-     * Request capabilities required by the widget
-     * @return {Promise} To be resolved with an array of requested widget capabilities
-     */
-    getCapabilities() {
-        console.log('Requesting capabilities for', this.widgetId);
-        return this.messageToWidget({
-                api: OUTBOUND_API_NAME,
-                action: "capabilities",
-            }).then((response) => {
-                console.log('Got capabilities for', this.widgetId, response.capabilities);
-                return response.capabilities;
-            });
-    }
-
-    sendVisibility(visible) {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: "visibility",
-            visible,
-        })
-        .catch((error) => {
-            console.error("Failed to send visibility: ", error);
-        });
-    }
-
-    start() {
-        this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
-        this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
-    }
-
-    stop() {
-        this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl);
-        this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
-    }
-
-    async _onOpenIdRequest(ev, rawEv) {
-        if (ev.widgetId !== this.widgetId) return; // not interesting
-
-        const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget);
-
-        const settings = SettingsStore.getValue("widgetOpenIDPermissions");
-        if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
-            this.fromWidget.sendResponse(rawEv, {state: "blocked"});
-            return;
-        }
-        if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
-            const responseBody = {state: "allowed"};
-            const credentials = await MatrixClientPeg.get().getOpenIdToken();
-            Object.assign(responseBody, credentials);
-            this.fromWidget.sendResponse(rawEv, responseBody);
-            return;
-        }
-
-        // Confirm that we received the request
-        this.fromWidget.sendResponse(rawEv, {state: "request"});
-
-        // Actually ask for permission to send the user's data
-        Modal.createTrackedDialog("OpenID widget permissions", '',
-            WidgetOpenIDPermissionsDialog, {
-                widgetUrl: this.wurl,
-                widgetId: this.widgetId,
-                isUserWidget: this.isUserWidget,
-
-                onFinished: async (confirm) => {
-                    const responseBody = {
-                        // Legacy (early draft) fields
-                        success: confirm,
-
-                        // New style MSC1960 fields
-                        state: confirm ? "allowed" : "blocked",
-                        original_request_id: ev.requestId, // eslint-disable-line camelcase
-                    };
-                    if (confirm) {
-                        const credentials = await MatrixClientPeg.get().getOpenIdToken();
-                        Object.assign(responseBody, credentials);
-                    }
-                    this.messageToWidget({
-                        api: OUTBOUND_API_NAME,
-                        action: "openid_credentials",
-                        data: responseBody,
-                    }).catch((error) => {
-                        console.error("Failed to send OpenID credentials: ", error);
-                    });
-                },
-            },
-        );
-    }
-}
diff --git a/src/WidgetMessagingEndpoint.js b/src/WidgetMessagingEndpoint.js
deleted file mode 100644
index 9114e12137..0000000000
--- a/src/WidgetMessagingEndpoint.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2018 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.
-*/
-
-
-/**
- * Represents mapping of widget instance to URLs for trusted postMessage communication.
- */
-export default class WidgetMessageEndpoint {
-    /**
-     * Mapping of widget instance to URL for trusted postMessage communication.
-     * @param  {string} widgetId    Unique widget identifier
-     * @param  {string} endpointUrl Widget wurl origin.
-     */
-    constructor(widgetId, endpointUrl) {
-        if (!widgetId) {
-            throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
-        }
-        if (!endpointUrl) {
-            throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
-        }
-        this.widgetId = widgetId;
-        this.endpointUrl = endpointUrl;
-    }
-}
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index e8ef4de257..df1fbe0f3c 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -36,12 +36,12 @@ 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 {SettingLevel} from "../../../settings/SettingLevel";
 import WidgetStore from "../../../stores/WidgetStore";
 import {Action} from "../../../dispatcher/actions";
 import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
 import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
+import {MatrixCapabilities} from "matrix-widget-api";
 
 export default class AppTile extends React.Component {
     constructor(props) {
@@ -305,7 +305,7 @@ export default class AppTile extends React.Component {
         if (payload.widgetId === this.props.app.id) {
             switch (payload.action) {
                 case 'm.sticker':
-                    if (this._sgWidget.widgetApi.hasCapability(Capability.Sticker)) {
+                    if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
                         dis.dispatch({action: 'post_sticker_message', data: payload.data});
                     } else {
                         console.warn('Ignoring sticker message. Invalid capability');
@@ -562,7 +562,7 @@ export default class AppTile extends React.Component {
             const canUserModify = this._canUserModify();
             const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
             const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
-            const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(Capability.Screenshot)
+            const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots)
                 && this.props.show;
 
             const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index 6bb45df109..8efbe3dcf3 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -36,11 +36,12 @@ import IconizedContextMenu, {
     IconizedContextMenuOptionList,
 } from "../context_menus/IconizedContextMenu";
 import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
-import {Capability} from "../../../widgets/WidgetApi";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import classNames from "classnames";
 import dis from "../../../dispatcher/dispatcher";
 import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
+import { Capability } from "matrix-widget-api/lib/interfaces/Capabilities";
+import { MatrixCapabilities } from "matrix-widget-api";
 
 interface IProps {
     room: Room;
@@ -80,7 +81,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
     if (menuDisplayed) {
         let snapshotButton;
         const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
-        if (widgetMessaging?.hasCapability(Capability.Screenshot)) {
+        if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
             const onSnapshotClick = () => {
                 widgetMessaging.takeScreenshot().then(data => {
                     dis.dispatch({
diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js
index d6aaf83196..4ae8dfeddb 100644
--- a/src/stores/ActiveWidgetStore.js
+++ b/src/stores/ActiveWidgetStore.js
@@ -66,14 +66,7 @@ class ActiveWidgetStore extends EventEmitter {
         if (id !== this._persistentWidgetId) return;
         const toDeleteId = this._persistentWidgetId;
 
-        const result = WidgetMessagingStore.instance.findWidgetById(id);
-        if (result) {
-            if (result.room) {
-                WidgetMessagingStore.instance.stopMessagingForRoomWidget(result.room, result.widget);
-            } else {
-                WidgetMessagingStore.instance.stopMessagingForAccountWidget(result.widget);
-            }
-        }
+        WidgetMessagingStore.instance.stopMessagingById(id);
 
         this.setWidgetPersistence(toDeleteId, false);
         this.delRoomId(toDeleteId);
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index cd66522488..76c027bb33 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -14,14 +14,18 @@
  * limitations under the License.
  */
 
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
 import {
-    ClientWidgetApi, IStickerActionRequest,
+    ClientWidgetApi,
+    IStickerActionRequest,
     IStickyActionRequest,
-    IWidget, IWidgetApiRequest,
+    IWidget,
+    IWidgetApiRequest,
     IWidgetApiRequestEmptyData,
     IWidgetData,
-    Widget, WidgetApiFromWidgetAction
+    MatrixCapabilities,
+    Widget,
+    WidgetApiFromWidgetAction
 } from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";
@@ -33,11 +37,9 @@ import WidgetUtils from '../../utils/WidgetUtils';
 import { IntegrationManagers } from "../../integrations/IntegrationManagers";
 import SettingsStore from "../../settings/SettingsStore";
 import { WidgetType } from "../../widgets/WidgetType";
-import { Capability } from "../../widgets/WidgetApi";
 import ActiveWidgetStore from "../ActiveWidgetStore";
 import { objectShallowClone } from "../../utils/objects";
 import defaultDispatcher from "../../dispatcher/dispatcher";
-import dis from "../../dispatcher/dispatcher";
 import { ElementWidgetActions } from "./ElementWidgetActions";
 
 // TODO: Destroy all of this code
@@ -171,7 +173,7 @@ export class StopGapWidget extends EventEmitter {
         if (WidgetType.JITSI.matches(this.mockWidget.type)) {
             this.messaging.addEventListener("action:set_always_on_screen",
                 (ev: CustomEvent<IStickyActionRequest>) => {
-                    if (this.messaging.hasCapability(Capability.AlwaysOnScreen)) {
+                    if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
                         ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
                         ev.preventDefault();
                         this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index 828465ce84..83d3ac7df8 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -61,6 +61,15 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
         return this.widgetMap.get(widget.id);
     }
 
+    /**
+     * Stops the widget messaging instance for a given widget ID.
+     * @param {string} widgetId The widget ID.
+     * @deprecated Widget IDs are not globally unique.
+     */
+    public stopMessagingById(widgetId: string) {
+        this.widgetMap.remove(widgetId)?.stop();
+    }
+
     /**
      * Gets the widget messaging class for a given widget ID.
      * @param {string} widgetId The widget ID.
diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index 9373738bf8..6cc95efb25 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -28,11 +28,11 @@ const WIDGET_WAIT_TIME = 20000;
 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";
 import {objectClone} from "./objects";
 import {_t} from "../languageHandler";
+import {MatrixCapabilities} from "matrix-widget-api";
 
 export default class WidgetUtils {
     /* Returns true if user is able to send state events to modify widgets in this room
@@ -416,13 +416,13 @@ export default class WidgetUtils {
     static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
         const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
 
-        const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
+        const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : [];
 
         // 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 (WidgetType.JITSI.matches(appType)) {
-            capWhitelist.push(Capability.AlwaysOnScreen);
+            capWhitelist.push(MatrixCapabilities.AlwaysOnScreen);
         }
 
         return capWhitelist;
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
deleted file mode 100644
index ab9604d155..0000000000
--- a/src/widgets/WidgetApi.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
-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.
-*/
-
-// Dev note: This is largely inspired by Dimension. Used with permission.
-// https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts
-
-import { randomString } from "matrix-js-sdk/src/randomstring";
-import { EventEmitter } from "events";
-import { objectClone } from "../utils/objects";
-
-export enum Capability {
-    Screenshot = "m.capability.screenshot",
-    Sticker = "m.sticker",
-    AlwaysOnScreen = "m.always_on_screen",
-}
-
-export enum KnownWidgetActions {
-    GetSupportedApiVersions = "supported_api_versions",
-    TakeScreenshot = "screenshot",
-    GetCapabilities = "capabilities",
-    SendEvent = "send_event",
-    UpdateVisibility = "visibility",
-    GetOpenIDCredentials = "get_openid",
-    ReceiveOpenIDCredentials = "openid_credentials",
-    SetAlwaysOnScreen = "set_always_on_screen",
-    ClientReady = "im.vector.ready",
-    Terminate = "im.vector.terminate",
-    Hangup = "im.vector.hangup",
-}
-
-export type WidgetAction = KnownWidgetActions | string;
-
-export enum WidgetApiType {
-    ToWidget = "toWidget",
-    FromWidget = "fromWidget",
-}
-
-export interface WidgetRequest {
-    api: WidgetApiType;
-    widgetId: string;
-    requestId: string;
-    data: any;
-    action: WidgetAction;
-}
-
-export interface ToWidgetRequest extends WidgetRequest {
-    api: WidgetApiType.ToWidget;
-}
-
-export interface FromWidgetRequest extends WidgetRequest {
-    api: WidgetApiType.FromWidget;
-    response: any;
-}
-
-export interface OpenIDCredentials {
-    accessToken: string;
-    tokenType: string;
-    matrixServerName: string;
-    expiresIn: number;
-}
-
-/**
- * Handles Element <--> Widget interactions for embedded/standalone widgets.
- *
- * Emitted events:
- * - terminate(wait): client requested the widget to terminate.
- *   Call the argument 'wait(promise)' to postpone the finalization until
- *   the given promise resolves.
- */
-export class WidgetApi extends EventEmitter {
-    private readonly origin: string;
-    private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
-    private readonly readyPromise: Promise<any>;
-    private readyPromiseResolve: () => void;
-    private openIDCredentialsCallback: () => void;
-    public openIDCredentials: OpenIDCredentials;
-
-    /**
-     * Set this to true if your widget is expecting a ready message from the client. False otherwise (default).
-     */
-    public expectingExplicitReady = false;
-
-    constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
-        super();
-
-        this.origin = new URL(currentUrl).origin;
-
-        this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
-
-        window.addEventListener("message", event => {
-            if (event.origin !== this.origin) return; // ignore: invalid origin
-            if (!event.data) return; // invalid schema
-            if (event.data.widgetId !== this.widgetId) return; // not for us
-
-            const payload = <WidgetRequest>event.data;
-            if (payload.api === WidgetApiType.ToWidget && payload.action) {
-                console.log(`[WidgetAPI] Got request: ${JSON.stringify(payload)}`);
-
-                if (payload.action === KnownWidgetActions.GetCapabilities) {
-                    this.onCapabilitiesRequest(<ToWidgetRequest>payload);
-                    if (!this.expectingExplicitReady) {
-                        this.readyPromiseResolve();
-                    }
-                } else if (payload.action === KnownWidgetActions.ClientReady) {
-                    this.readyPromiseResolve();
-
-                    // Automatically acknowledge so we can move on
-                    this.replyToRequest(<ToWidgetRequest>payload, {});
-                } else if (payload.action === KnownWidgetActions.Terminate
-                    || payload.action === KnownWidgetActions.Hangup) {
-                    // Finalization needs to be async, so postpone with a promise
-                    let finalizePromise = Promise.resolve();
-                    const wait = (promise) => {
-                        finalizePromise = finalizePromise.then(() => promise);
-                    };
-                    const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup';
-                    this.emit(emitName, wait);
-                    Promise.resolve(finalizePromise).then(() => {
-                        // Acknowledge that we're shut down now
-                        this.replyToRequest(<ToWidgetRequest>payload, {});
-                    });
-                } else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) {
-                    // Save OpenID credentials
-                    this.setOpenIDCredentials(<ToWidgetRequest>payload);
-                    this.replyToRequest(<ToWidgetRequest>payload, {});
-                } else {
-                    console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
-                }
-            } else if (payload.api === WidgetApiType.FromWidget && this.inFlightRequests[payload.requestId]) {
-                console.log(`[WidgetAPI] Got reply: ${JSON.stringify(payload)}`);
-                const handler = this.inFlightRequests[payload.requestId];
-                delete this.inFlightRequests[payload.requestId];
-                handler(<FromWidgetRequest>payload);
-            } else {
-                console.warn(`[WidgetAPI] Unhandled payload: ${JSON.stringify(payload)}`);
-            }
-        });
-    }
-
-    public setOpenIDCredentials(value: WidgetRequest) {
-        const data = value.data;
-        if (data.state === 'allowed') {
-            this.openIDCredentials = {
-                accessToken: data.access_token,
-                tokenType: data.token_type,
-                matrixServerName: data.matrix_server_name,
-                expiresIn: data.expires_in,
-            }
-        } else if (data.state === 'blocked') {
-            this.openIDCredentials = null;
-        }
-        if (['allowed', 'blocked'].includes(data.state) && this.openIDCredentialsCallback) {
-            this.openIDCredentialsCallback()
-        }
-    }
-
-    public requestOpenIDCredentials(credentialsResponseCallback: () => void) {
-        this.openIDCredentialsCallback = credentialsResponseCallback;
-        this.callAction(
-            KnownWidgetActions.GetOpenIDCredentials,
-            {},
-            this.setOpenIDCredentials,
-        );
-    }
-
-    public waitReady(): Promise<any> {
-        return this.readyPromise;
-    }
-
-    private replyToRequest(payload: ToWidgetRequest, reply: any) {
-        if (!window.parent) return;
-
-        const request: ToWidgetRequest & {response?: any} = objectClone(payload);
-        request.response = reply;
-
-        window.parent.postMessage(request, this.origin);
-    }
-
-    private onCapabilitiesRequest(payload: ToWidgetRequest) {
-        return this.replyToRequest(payload, {capabilities: this.requestedCapabilities});
-    }
-
-    public callAction(action: WidgetAction, payload: any, callback: (reply: FromWidgetRequest) => void) {
-        if (!window.parent) return;
-
-        const request: FromWidgetRequest = {
-            api: WidgetApiType.FromWidget,
-            widgetId: this.widgetId,
-            action: action,
-            requestId: randomString(160),
-            data: payload,
-            response: {}, // Not used at this layer - it's used when the client responds
-        };
-
-        if (callback) {
-            this.inFlightRequests[request.requestId] = callback;
-        }
-
-        console.log(`[WidgetAPI] Sending request: `, request);
-        window.parent.postMessage(request, "*");
-    }
-
-    public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
-        return new Promise<any>(resolve => {
-            this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null);
-            resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
-        });
-    }
-}
diff --git a/src/widgets/WidgetType.ts b/src/widgets/WidgetType.ts
index e4b37e639c..e42f3ffa9b 100644
--- a/src/widgets/WidgetType.ts
+++ b/src/widgets/WidgetType.ts
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+// TODO: Move to matrix-widget-api
 export class WidgetType {
     public static readonly JITSI = new WidgetType("m.jitsi", "jitsi");
     public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");

From 08c5e9e039777dcb4c869eb829d7ec57ab85180e Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:42:37 -0600
Subject: [PATCH 18/21] Use the beta release of the widget-api

---
 package.json | 1 +
 yarn.lock    | 5 +++++
 2 files changed, 6 insertions(+)

diff --git a/package.json b/package.json
index 3ab523ee9a..e66d0aabcf 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
     "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
+    "matrix-widget-api": "^0.1.0-beta.2",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
     "parse5": "^5.1.1",
diff --git a/yarn.lock b/yarn.lock
index 9ecf43d7a4..51ff681783 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5953,6 +5953,11 @@ matrix-react-test-utils@^0.2.2:
   resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
   integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
 
+matrix-widget-api@^0.1.0-beta.2:
+  version "0.1.0-beta.2"
+  resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.2.tgz#367da1ccd26b711f73fc5b6e02edf55ac2ea2692"
+  integrity sha512-q5g5RZN+RRjM4HmcJ+LYoQAYrB1wzyERmoQ+LvKbTV/+9Ov36Kp0QEP8CleSXEd5WLp6bkRlt60axDaY6pWGmg==
+
 mdast-util-compact@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz#d531bb7667b5123abf20859be086c4d06c894593"

From 2ec94e8a699c6288c5fa30f355c0e685ef515014 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:49:31 -0600
Subject: [PATCH 19/21] Appease the linter

---
 src/components/views/elements/AppTile.js        | 4 ++--
 src/components/views/right_panel/WidgetCard.tsx | 2 --
 src/components/views/rooms/Stickerpicker.js     | 1 -
 src/stores/OwnProfileStore.ts                   | 2 +-
 src/stores/widgets/StopGapWidget.ts             | 2 +-
 5 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index df1fbe0f3c..5fe8b50b64 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -150,7 +150,7 @@ export default class AppTile extends React.Component {
             this.setState({initialising: false});
         });
     }
-    
+
     _iframeRefChange = (ref) => {
         this.setState({iframe: ref});
         if (ref) {
@@ -223,7 +223,7 @@ export default class AppTile extends React.Component {
      * @private
      * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
      */
-    _endWidgetActions() {
+    async _endWidgetActions() { // widget migration dev note: async to maintain signature
         // 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
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index 8efbe3dcf3..30900b9a4d 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -29,7 +29,6 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
 import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
 import {Action} from "../../../dispatcher/actions";
 import WidgetStore from "../../../stores/WidgetStore";
-import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
 import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
 import IconizedContextMenu, {
     IconizedContextMenuOption,
@@ -40,7 +39,6 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import classNames from "classnames";
 import dis from "../../../dispatcher/dispatcher";
 import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
-import { Capability } from "matrix-widget-api/lib/interfaces/Capabilities";
 import { MatrixCapabilities } from "matrix-widget-api";
 
 interface IProps {
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index d191e05407..2faa0fea27 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -22,7 +22,6 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import AccessibleButton from '../elements/AccessibleButton';
 import WidgetUtils from '../../../utils/WidgetUtils';
-import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
 import PersistedElement from "../elements/PersistedElement";
 import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
 import SettingsStore from "../../../settings/SettingsStore";
diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts
index 61387e3c26..8983380fec 100644
--- a/src/stores/OwnProfileStore.ts
+++ b/src/stores/OwnProfileStore.ts
@@ -70,7 +70,7 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
      * will be returned as an HTTP URL.
      * @returns The HTTP URL of the user's avatar
      */
-    public getHttpAvatarUrl(size: number = 0): string {
+    public getHttpAvatarUrl(size = 0): string {
         if (!this.avatarMxc) return null;
         const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
         return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 76c027bb33..1c24f70d0d 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -25,7 +25,7 @@ import {
     IWidgetData,
     MatrixCapabilities,
     Widget,
-    WidgetApiFromWidgetAction
+    WidgetApiFromWidgetAction,
 } from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";

From 0570deffa2d026e56a6e62e9b49407aacbead399 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 1 Oct 2020 10:01:13 -0600
Subject: [PATCH 20/21] Fix iterableUnion types

---
 src/stores/widgets/StopGapWidgetDriver.ts | 2 +-
 src/utils/iterables.ts                    | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index 84626e74fb..b54e4a5f7d 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -25,6 +25,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
     }
 
     public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
-        return iterableUnion(requested, new Set(this.allowedCapabilities));
+        return new Set(iterableUnion(requested, this.allowedCapabilities));
     }
 }
diff --git a/src/utils/iterables.ts b/src/utils/iterables.ts
index 3d2585906d..56e0bca1b7 100644
--- a/src/utils/iterables.ts
+++ b/src/utils/iterables.ts
@@ -16,6 +16,6 @@
 
 import { arrayUnion } from "./arrays";
 
-export function iterableUnion<C extends Iterable<T>, T>(a: C, b: C): Set<T> {
-    return new Set(arrayUnion(Array.from(a), Array.from(b)));
+export function iterableUnion<T>(a: Iterable<T>, b: Iterable<T>): Iterable<T> {
+    return arrayUnion(Array.from(a), Array.from(b));
 }

From d64049059507bda2420e323e4d5366ff6af76e3b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 1 Oct 2020 10:03:52 -0600
Subject: [PATCH 21/21] Fix iframe reference

---
 src/components/views/elements/AppTile.js | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 5fe8b50b64..3945eaa763 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -51,6 +51,7 @@ export default class AppTile extends React.Component {
         this._persistKey = 'widget_' + this.props.app.id;
         this._sgWidget = new StopGapWidget(this.props);
         this._sgWidget.on("ready", this._onWidgetReady);
+        this.iframe = null; // ref to the iframe (callback style)
 
         this.state = this._getNewState(props);
 
@@ -152,7 +153,7 @@ export default class AppTile extends React.Component {
     }
 
     _iframeRefChange = (ref) => {
-        this.setState({iframe: ref});
+        this.iframe = ref;
         if (ref) {
             this._sgWidget.start(ref);
         } else {
@@ -227,14 +228,14 @@ export default class AppTile extends React.Component {
         // 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.state.iframe) {
+        if (this.iframe) {
             // 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.state.iframe.src = 'about:blank';
+            this.iframe.src = 'about:blank';
         }
 
         // Delete the widget from the persisted store for good measure.
@@ -425,9 +426,9 @@ export default class AppTile extends React.Component {
         // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
         if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
             this._endWidgetActions().then(() => {
-                if (this.state.iframe) {
+                if (this.iframe) {
                     // Reload iframe
-                    this.state.iframe.src = this._sgWidget.embedUrl;
+                    this.iframe.src = this._sgWidget.embedUrl;
                     this.setState({});
                 }
             });
@@ -441,7 +442,7 @@ export default class AppTile extends React.Component {
     _onReloadWidgetClick() {
         // Reload iframe in this way to avoid cross-origin restrictions
         // eslint-disable-next-line no-self-assign
-        this.state.iframe.src = this.state.iframe.src;
+        this.iframe.src = this.iframe.src;
     }
 
     _onContextMenuClick = () => {