From 342f1d5b438710daafc0ee0f4c1a94af8dcbf9ee Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 21 Sep 2020 14:36:16 -0600
Subject: [PATCH 01/11] Extremely bad support for "temporary widgets"

---
 src/FromWidgetPostMessageApi.js               |  10 +-
 src/Modal.tsx                                 |   2 +-
 src/WidgetMessaging.js                        |  30 ++++
 .../views/dialogs/TempWidgetDialog.tsx        | 155 ++++++++++++++++++
 src/i18n/strings/en_EN.json                   |   4 +
 src/stores/TempWidgetStore.ts                 |  54 ++++++
 src/widgets/WidgetApi.ts                      |  38 ++++-
 7 files changed, 287 insertions(+), 6 deletions(-)
 create mode 100644 src/components/views/dialogs/TempWidgetDialog.tsx
 create mode 100644 src/stores/TempWidgetStore.ts

diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
index d5d7c08d50..c5a25468a8 100644
--- a/src/FromWidgetPostMessageApi.js
+++ b/src/FromWidgetPostMessageApi.js
@@ -24,8 +24,10 @@ 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 {Capability, KnownWidgetActions} from "./widgets/WidgetApi";
 import {objectClone} from "./utils/objects";
+import {Action} from "./dispatcher/actions";
+import {TempWidgetStore} from "./stores/TempWidgetStore";
 
 const WIDGET_API_VERSION = '0.0.2'; // Current API version
 const SUPPORTED_WIDGET_API_VERSIONS = [
@@ -218,8 +220,12 @@ export default class FromWidgetPostMessageApi {
             if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
                 ActiveWidgetStore.setWidgetPersistence(widgetId, val);
             }
-        } else if (action === 'get_openid') {
+        } else if (action === 'get_openid'
+            || action === KnownWidgetActions.CloseWidget) {
             // Handled by caller
+        } else if (action === KnownWidgetActions.OpenTempWidget) {
+            TempWidgetStore.instance.openTempWidget(event.data.data, widgetId);
+            this.sendResponse(event, {}); // ack
         } else {
             console.warn('Widget postMessage event unhandled');
             this.sendError(event, {message: 'The postMessage was unhandled'});
diff --git a/src/Modal.tsx b/src/Modal.tsx
index 0a36813961..3d95bc1a2b 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper';
 const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
 const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
 
-interface IModal<T extends any[]> {
+export interface IModal<T extends any[]> {
     elem: React.ReactNode;
     className?: string;
     beforeClosePromise?: Promise<boolean>;
diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
index c68e926ac1..6a2eeb852c 100644
--- a/src/WidgetMessaging.js
+++ b/src/WidgetMessaging.js
@@ -147,6 +147,36 @@ export default class WidgetMessaging {
         });
     }
 
+    sendThemeInfo(themeInfo: any) {
+        return this.messageToWidget({
+            api: OUTBOUND_API_NAME,
+            action: KnownWidgetActions.UpdateThemeInfo,
+            data: themeInfo,
+        }).catch((error) => {
+            console.error("Failed to send theme info: ", error);
+        });
+    }
+
+    sendWidgetConfig(widgetConfig: any) {
+        return this.messageToWidget({
+            api: OUTBOUND_API_NAME,
+            action: KnownWidgetActions.SendWidgetConfig,
+            data: widgetConfig,
+        }).catch((error) => {
+            console.error("Failed to send widget info: ", error);
+        });
+    }
+
+    sendTempCloseInfo(info: any) {
+        return this.messageToWidget({
+            api: OUTBOUND_API_NAME,
+            action: KnownWidgetActions.ClosedWidgetResponse,
+            data: info,
+        }).catch((error) => {
+            console.error("Failed to send temp widget close info: ", error);
+        });
+    }
+
     start() {
         this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
         this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
diff --git a/src/components/views/dialogs/TempWidgetDialog.tsx b/src/components/views/dialogs/TempWidgetDialog.tsx
new file mode 100644
index 0000000000..1fd3b26b5c
--- /dev/null
+++ b/src/components/views/dialogs/TempWidgetDialog.tsx
@@ -0,0 +1,155 @@
+/*
+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 * as React from 'react';
+import BaseDialog from './BaseDialog';
+import { _t } from '../../../languageHandler';
+import { IDialogProps } from "./IDialogProps";
+import WidgetMessaging from "../../../WidgetMessaging";
+import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
+import Field from "../elements/Field";
+import { KnownWidgetActions } from "../../../widgets/WidgetApi";
+import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
+
+interface IState {
+    messaging?: WidgetMessaging;
+
+    androidMode: boolean;
+    darkTheme: boolean;
+    accentColor: string;
+}
+
+interface IProps extends IDialogProps {
+    widgetDefinition: {url: string, data: any};
+    sourceWidgetId: string;
+}
+
+// TODO: Make a better dialog
+
+export default class TempWidgetDialog extends React.PureComponent<IProps, IState> {
+    private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
+
+    constructor(props) {
+        super(props);
+        this.state = {
+            androidMode: false,
+            darkTheme: false,
+            accentColor: "#03b381",
+        };
+    }
+
+    public componentDidMount() {
+        // TODO: Don't violate every principle of widget creation
+        const messaging = new WidgetMessaging(
+            "TEMP_ID",
+            this.props.widgetDefinition.url,
+            this.props.widgetDefinition.url,
+            false,
+            this.appFrame.current.contentWindow,
+        );
+        this.setState({messaging});
+    }
+
+    public componentWillUnmount() {
+        this.state.messaging.fromWidget.removeListener(KnownWidgetActions.CloseWidget, this.onWidgetClose);
+        this.state.messaging.stop();
+    }
+
+    private onLoad = () => {
+        this.state.messaging.getCapabilities().then(caps => {
+            console.log("Requested capabilities: ", caps);
+            this.sendTheme();
+            this.state.messaging.sendWidgetConfig(this.props.widgetDefinition.data);
+        });
+        this.state.messaging.fromWidget.addListener(KnownWidgetActions.CloseWidget, this.onWidgetClose);
+    };
+
+    private sendTheme() {
+        if (!this.state.messaging) return;
+        this.state.messaging.sendThemeInfo({
+            clientName: this.state.androidMode ? "element-android" : "element-web",
+            isDark: this.state.darkTheme,
+            accentColor: this.state.accentColor,
+        });
+    }
+
+    public static sendExitData(sourceWidgetId: string, success: boolean, data?: any) {
+        const sourceMessaging = ActiveWidgetStore.getWidgetMessaging(sourceWidgetId);
+        if (!sourceMessaging) {
+            console.error("No source widget messaging for temp widget");
+            return;
+        }
+        sourceMessaging.sendTempCloseInfo({success, ...data});
+    }
+
+    private onWidgetClose = (req) => {
+        this.props.onFinished(true);
+        TempWidgetDialog.sendExitData(this.props.sourceWidgetId, true, req.data);
+    }
+
+    private onClientToggleChanged = (androidMode) => {
+        this.setState({androidMode}, () => this.sendTheme());
+    };
+
+    private onDarkThemeChanged = (darkTheme) => {
+        this.setState({darkTheme}, () => this.sendTheme());
+    };
+
+    private onAccentColorChanged = (ev) => {
+        this.setState({accentColor: ev.target.value}, () => this.sendTheme());
+    };
+
+    public render() {
+        // TODO: Don't violate every single security principle
+
+        const widgetUrl = this.props.widgetDefinition.url
+            + "?widgetId=TEMP_ID&parentUrl=" + encodeURIComponent(window.location.href);
+
+        return <BaseDialog
+            title={_t("Widget Proof of Concept Dashboard")}
+            className='mx_TempWidgetDialog'
+            contentId='mx_Dialog_content'
+            onFinished={this.props.onFinished}
+            hasCancel={false}
+        >
+            <div>
+                <LabelledToggleSwitch
+                    label={ _t("Look like Android")}
+                    onChange={this.onClientToggleChanged}
+                    value={this.state.androidMode}
+                />
+                <LabelledToggleSwitch
+                    label={ _t("Look like dark theme")}
+                    onChange={this.onDarkThemeChanged}
+                    value={this.state.darkTheme}
+                />
+                <Field
+                    value={this.state.accentColor}
+                    label={_t('Accent Colour')}
+                    onChange={this.onAccentColorChanged}
+                />
+            </div>
+            <div>
+                <iframe
+                    ref={this.appFrame}
+                    width={700} height={450}
+                    src={widgetUrl}
+                    onLoad={this.onLoad}
+                />
+            </div>
+        </BaseDialog>;
+    }
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d7360430ae..2da00d9317 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1844,6 +1844,10 @@
     "Missing session data": "Missing session data",
     "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
     "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.",
+    "Widget Proof of Concept Dashboard": "Widget Proof of Concept Dashboard",
+    "Look like Android": "Look like Android",
+    "Look like dark theme": "Look like dark theme",
+    "Accent Colour": "Accent Colour",
     "Integration Manager": "Integration Manager",
     "Find others by phone or email": "Find others by phone or email",
     "Be found by phone or email": "Be found by phone or email",
diff --git a/src/stores/TempWidgetStore.ts b/src/stores/TempWidgetStore.ts
new file mode 100644
index 0000000000..9f75963fd1
--- /dev/null
+++ b/src/stores/TempWidgetStore.ts
@@ -0,0 +1,54 @@
+/*
+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 { AsyncStoreWithClient } from "./AsyncStoreWithClient";
+import defaultDispatcher from "../dispatcher/dispatcher";
+import { ActionPayload } from "../dispatcher/payloads";
+import Modal, { IModal } from "../Modal";
+import TempWidgetDialog from "../components/views/dialogs/TempWidgetDialog";
+
+interface IState {
+    modal?: IModal<any>;
+    openedFromId?: string;
+}
+
+export class TempWidgetStore extends AsyncStoreWithClient<IState> {
+    private static internalInstance = new TempWidgetStore();
+
+    private constructor() {
+        super(defaultDispatcher, {});
+    }
+
+    public static get instance(): TempWidgetStore {
+        return TempWidgetStore.internalInstance;
+    }
+
+    protected async onAction(payload: ActionPayload): Promise<any> {
+        // nothing
+    }
+
+    public openTempWidget(requestData: any, sourceWidgetId: string) {
+        Modal.createTrackedDialog('Temp Widget', '', TempWidgetDialog, {
+            widgetDefinition: {...requestData},
+            sourceWidgetId: sourceWidgetId,
+            onFinished: (success) => {
+                if (!success) {
+                    TempWidgetDialog.sendExitData(sourceWidgetId, false);
+                }
+            },
+        });
+    }
+}
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
index 672cbf2a56..12bedb04cd 100644
--- a/src/widgets/WidgetApi.ts
+++ b/src/widgets/WidgetApi.ts
@@ -39,6 +39,12 @@ export enum KnownWidgetActions {
     SetAlwaysOnScreen = "set_always_on_screen",
     ClientReady = "im.vector.ready",
     Terminate = "im.vector.terminate",
+
+    OpenTempWidget = "io.element.start_temp",
+    UpdateThemeInfo = "io.element.theme_info",
+    SendWidgetConfig = "io.element.widget_config",
+    CloseWidget = "io.element.exit",
+    ClosedWidgetResponse = "io.element.exit_response",
 }
 
 export type WidgetAction = KnownWidgetActions | string;
@@ -134,6 +140,19 @@ export class WidgetApi extends EventEmitter {
                     // Save OpenID credentials
                     this.setOpenIDCredentials(<ToWidgetRequest>payload);
                     this.replyToRequest(<ToWidgetRequest>payload, {});
+                } else if (payload.action === KnownWidgetActions.UpdateThemeInfo
+                    || payload.action === KnownWidgetActions.SendWidgetConfig
+                    || payload.action === KnownWidgetActions.ClosedWidgetResponse) {
+                    // Finalization needs to be async, so postpone with a promise
+                    let finalizePromise = Promise.resolve();
+                    const wait = (promise) => {
+                        finalizePromise = finalizePromise.then(() => promise);
+                    };
+                    this.emit(payload.action, payload, wait);
+                    Promise.resolve(finalizePromise).then(() => {
+                        // Acknowledge that we're shut down now
+                        this.replyToRequest(<ToWidgetRequest>payload, {});
+                    });
                 } else {
                     console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
                 }
@@ -203,9 +222,8 @@ export class WidgetApi extends EventEmitter {
             response: {}, // Not used at this layer - it's used when the client responds
         };
 
-        if (callback) {
-            this.inFlightRequests[request.requestId] = callback;
-        }
+        if (!callback) callback = () => {}; // noop
+        this.inFlightRequests[request.requestId] = callback;
 
         console.log(`[WidgetAPI] Sending request: `, request);
         window.parent.postMessage(request, "*");
@@ -217,4 +235,18 @@ export class WidgetApi extends EventEmitter {
             resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
         });
     }
+
+    public closeWidget(exitData: any): Promise<any> {
+        return new Promise<any>(resolve => {
+            this.callAction(KnownWidgetActions.CloseWidget, exitData, null);
+            resolve();
+        });
+    }
+
+    public openTempWidget(url: string, data: any): Promise<any> {
+        return new Promise<any>(resolve => {
+            this.callAction(KnownWidgetActions.OpenTempWidget, {url, data}, null);
+            resolve();
+        });
+    }
 }

From 44bc8fc67ed0a313db08d6ba8979a5a71a16e419 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 25 Sep 2020 14:08:27 +0100
Subject: [PATCH 02/11] Initial Modal Widget work tweaks MSC2790

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 res/css/_components.scss                      |   1 +
 res/css/views/dialogs/_ModalWidgetDialog.scss |  23 +++
 src/FromWidgetPostMessageApi.js               |  15 +-
 src/Modal.tsx                                 |   2 +-
 src/WidgetMessaging.js                        |  28 ++--
 .../views/dialogs/ModalWidgetDialog.tsx       | 138 ++++++++++++++++
 .../views/dialogs/TempWidgetDialog.tsx        | 155 ------------------
 src/stores/ModalWidgetStore.ts                |  86 ++++++++++
 src/stores/TempWidgetStore.ts                 |  54 ------
 src/widgets/WidgetApi.ts                      |  35 ++--
 10 files changed, 294 insertions(+), 243 deletions(-)
 create mode 100644 res/css/views/dialogs/_ModalWidgetDialog.scss
 create mode 100644 src/components/views/dialogs/ModalWidgetDialog.tsx
 delete mode 100644 src/components/views/dialogs/TempWidgetDialog.tsx
 create mode 100644 src/stores/ModalWidgetStore.ts
 delete mode 100644 src/stores/TempWidgetStore.ts

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 35b4c1b965..285f3740aa 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -74,6 +74,7 @@
 @import "./views/dialogs/_InviteDialog.scss";
 @import "./views/dialogs/_KeyboardShortcutsDialog.scss";
 @import "./views/dialogs/_MessageEditHistoryDialog.scss";
+@import "./views/dialogs/_ModalWidgetDialog.scss";
 @import "./views/dialogs/_NewSessionReviewDialog.scss";
 @import "./views/dialogs/_RoomSettingsDialog.scss";
 @import "./views/dialogs/_RoomSettingsDialogBridges.scss";
diff --git a/res/css/views/dialogs/_ModalWidgetDialog.scss b/res/css/views/dialogs/_ModalWidgetDialog.scss
new file mode 100644
index 0000000000..f0eb19edb3
--- /dev/null
+++ b/res/css/views/dialogs/_ModalWidgetDialog.scss
@@ -0,0 +1,23 @@
+/*
+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.
+*/
+
+.mx_ModalWidgetDialog {
+    .mx_ModalWidgetDialog_buttons {
+        .mx_AccessibleButton + .mx_AccessibleButton {
+            margin-left: 8px;
+        }
+    }
+}
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
index c5a25468a8..f41cf0543d 100644
--- a/src/FromWidgetPostMessageApi.js
+++ b/src/FromWidgetPostMessageApi.js
@@ -26,8 +26,7 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
 import SettingsStore from "./settings/SettingsStore";
 import {Capability, KnownWidgetActions} from "./widgets/WidgetApi";
 import {objectClone} from "./utils/objects";
-import {Action} from "./dispatcher/actions";
-import {TempWidgetStore} from "./stores/TempWidgetStore";
+import {ModalWidgetStore} from "./stores/ModalWidgetStore";
 
 const WIDGET_API_VERSION = '0.0.2'; // Current API version
 const SUPPORTED_WIDGET_API_VERSIONS = [
@@ -220,12 +219,16 @@ export default class FromWidgetPostMessageApi {
             if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
                 ActiveWidgetStore.setWidgetPersistence(widgetId, val);
             }
-        } else if (action === 'get_openid'
-            || action === KnownWidgetActions.CloseWidget) {
+        } else if (action === 'get_openid' || action === KnownWidgetActions.CloseModalWidget) {
             // Handled by caller
-        } else if (action === KnownWidgetActions.OpenTempWidget) {
-            TempWidgetStore.instance.openTempWidget(event.data.data, widgetId);
             this.sendResponse(event, {}); // ack
+        } else if (action === KnownWidgetActions.OpenModalWidget) {
+            if (ModalWidgetStore.instance.canOpenModalWidget()) {
+                ModalWidgetStore.instance.openModalWidget(event.data.data, widgetId);
+                this.sendResponse(event, {}); // ack
+            } else {
+                this.sendError(event, {message: 'Unable to open modal at this time'}); // nak
+            }
         } else {
             console.warn('Widget postMessage event unhandled');
             this.sendError(event, {message: 'The postMessage was unhandled'});
diff --git a/src/Modal.tsx b/src/Modal.tsx
index 3d95bc1a2b..93d85cfb15 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -38,7 +38,7 @@ export interface IModal<T extends any[]> {
     close(...args: T): void;
 }
 
-interface IHandle<T extends any[]> {
+export interface IHandle<T extends any[]> {
     finished: Promise<T>;
     close(...args: T): void;
 }
diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
index 6a2eeb852c..519b3f80b5 100644
--- a/src/WidgetMessaging.js
+++ b/src/WidgetMessaging.js
@@ -147,33 +147,33 @@ export default class WidgetMessaging {
         });
     }
 
-    sendThemeInfo(themeInfo: any) {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.UpdateThemeInfo,
-            data: themeInfo,
-        }).catch((error) => {
-            console.error("Failed to send theme info: ", error);
-        });
-    }
-
     sendWidgetConfig(widgetConfig: any) {
         return this.messageToWidget({
             api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.SendWidgetConfig,
+            action: KnownWidgetActions.GetWidgetConfig,
             data: widgetConfig,
         }).catch((error) => {
             console.error("Failed to send widget info: ", error);
         });
     }
 
-    sendTempCloseInfo(info: any) {
+    sendModalButtonClicked(id: string) {
         return this.messageToWidget({
             api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.ClosedWidgetResponse,
+            action: KnownWidgetActions.ButtonClicked,
+            data: {id},
+        }).catch((error) => {
+            console.error("Failed to send modal widget button clicked: ", error);
+        });
+    }
+
+    sendModalCloseInfo(info: any) {
+        return this.messageToWidget({
+            api: OUTBOUND_API_NAME,
+            action: KnownWidgetActions.CloseModalWidget,
             data: info,
         }).catch((error) => {
-            console.error("Failed to send temp widget close info: ", error);
+            console.error("Failed to send modal widget close info: ", error);
         });
     }
 
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
new file mode 100644
index 0000000000..d2599d6f90
--- /dev/null
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -0,0 +1,138 @@
+/*
+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 * as React from 'react';
+import BaseDialog from './BaseDialog';
+import { _t } from '../../../languageHandler';
+import WidgetMessaging from "../../../WidgetMessaging";
+import {ButtonKind, IButton, KnownWidgetActions} from "../../../widgets/WidgetApi";
+import AccessibleButton from "../elements/AccessibleButton";
+
+interface IModalWidget {
+    type: string;
+    url: string;
+    name: string;
+    data: any;
+    waitForIframeLoad?: boolean;
+    buttons?: IButton[];
+}
+
+interface IProps {
+    widgetDefinition: IModalWidget;
+    sourceWidgetId: string;
+    onFinished(success: boolean, data?: any): void;
+}
+
+interface IState {
+    messaging?: WidgetMessaging;
+}
+
+const MAX_BUTTONS = 3;
+
+export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
+    private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
+
+    state: IState = {};
+
+    private getWidgetId() {
+        return `modal_${this.props.sourceWidgetId}`;
+    }
+
+    public componentDidMount() {
+        // TODO: Don't violate every principle of widget creation
+        const messaging = new WidgetMessaging(
+            this.getWidgetId(),
+            this.props.widgetDefinition.url,
+            this.props.widgetDefinition.url, // TODO templating and such
+            true,
+            this.appFrame.current.contentWindow,
+        );
+        this.setState({messaging});
+    }
+
+    public componentWillUnmount() {
+        this.state.messaging.fromWidget.removeListener(KnownWidgetActions.CloseModalWidget, this.onWidgetClose);
+        this.state.messaging.stop();
+    }
+
+    private onLoad = () => {
+        this.state.messaging.getCapabilities().then(caps => {
+            console.log("Requested capabilities: ", caps);
+            this.state.messaging.sendWidgetConfig(this.props.widgetDefinition.data);
+        });
+        this.state.messaging.fromWidget.addListener(KnownWidgetActions.CloseModalWidget, this.onWidgetClose);
+    };
+
+    private onWidgetClose = (req) => {
+        this.props.onFinished(true, req.data);
+    }
+
+    public render() {
+        // TODO: Don't violate every single security principle
+
+        const widgetUrl = this.props.widgetDefinition.url
+            + `?widgetId=${this.getWidgetId()}&parentUrl=${encodeURIComponent(window.location.href)}`;
+
+        let buttons;
+        if (this.props.widgetDefinition.buttons) {
+            // show first button rightmost for a more natural specification
+            buttons = this.props.widgetDefinition.buttons.slice(0, MAX_BUTTONS).reverse().map(def => {
+                let kind = "secondary";
+                switch (def.kind) {
+                    case ButtonKind.Primary:
+                        kind = "primary";
+                        break;
+                    case ButtonKind.Secondary:
+                        kind = "primary_outline";
+                        break
+                    case ButtonKind.Danger:
+                        kind = "danger";
+                        break;
+                }
+
+                const onClick = () => {
+                    this.state.messaging.sendModalButtonClicked(def.id);
+                };
+
+                return <AccessibleButton key={def.id} kind={kind} onClick={onClick}>
+                    { def.label }
+                </AccessibleButton>;
+            });
+        }
+
+        return <BaseDialog
+            title={this.props.widgetDefinition.name || _t("Modal Widget")}
+            className="mx_ModalWidgetDialog"
+            contentId="mx_Dialog_content"
+            onFinished={this.props.onFinished}
+            hasCancel={false}
+        >
+            <div>
+                <iframe
+                    ref={this.appFrame}
+                    sandbox="allow-forms allow-scripts"
+                    width={700} // TODO
+                    height={450} // TODO
+                    src={widgetUrl}
+                    onLoad={this.onLoad}
+                />
+            </div>
+            <div className="mx_ModalWidgetDialog_buttons" style={{float: "right"}}>
+                { buttons }
+            </div>
+        </BaseDialog>;
+    }
+}
diff --git a/src/components/views/dialogs/TempWidgetDialog.tsx b/src/components/views/dialogs/TempWidgetDialog.tsx
deleted file mode 100644
index 1fd3b26b5c..0000000000
--- a/src/components/views/dialogs/TempWidgetDialog.tsx
+++ /dev/null
@@ -1,155 +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 * as React from 'react';
-import BaseDialog from './BaseDialog';
-import { _t } from '../../../languageHandler';
-import { IDialogProps } from "./IDialogProps";
-import WidgetMessaging from "../../../WidgetMessaging";
-import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
-import Field from "../elements/Field";
-import { KnownWidgetActions } from "../../../widgets/WidgetApi";
-import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
-
-interface IState {
-    messaging?: WidgetMessaging;
-
-    androidMode: boolean;
-    darkTheme: boolean;
-    accentColor: string;
-}
-
-interface IProps extends IDialogProps {
-    widgetDefinition: {url: string, data: any};
-    sourceWidgetId: string;
-}
-
-// TODO: Make a better dialog
-
-export default class TempWidgetDialog extends React.PureComponent<IProps, IState> {
-    private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
-
-    constructor(props) {
-        super(props);
-        this.state = {
-            androidMode: false,
-            darkTheme: false,
-            accentColor: "#03b381",
-        };
-    }
-
-    public componentDidMount() {
-        // TODO: Don't violate every principle of widget creation
-        const messaging = new WidgetMessaging(
-            "TEMP_ID",
-            this.props.widgetDefinition.url,
-            this.props.widgetDefinition.url,
-            false,
-            this.appFrame.current.contentWindow,
-        );
-        this.setState({messaging});
-    }
-
-    public componentWillUnmount() {
-        this.state.messaging.fromWidget.removeListener(KnownWidgetActions.CloseWidget, this.onWidgetClose);
-        this.state.messaging.stop();
-    }
-
-    private onLoad = () => {
-        this.state.messaging.getCapabilities().then(caps => {
-            console.log("Requested capabilities: ", caps);
-            this.sendTheme();
-            this.state.messaging.sendWidgetConfig(this.props.widgetDefinition.data);
-        });
-        this.state.messaging.fromWidget.addListener(KnownWidgetActions.CloseWidget, this.onWidgetClose);
-    };
-
-    private sendTheme() {
-        if (!this.state.messaging) return;
-        this.state.messaging.sendThemeInfo({
-            clientName: this.state.androidMode ? "element-android" : "element-web",
-            isDark: this.state.darkTheme,
-            accentColor: this.state.accentColor,
-        });
-    }
-
-    public static sendExitData(sourceWidgetId: string, success: boolean, data?: any) {
-        const sourceMessaging = ActiveWidgetStore.getWidgetMessaging(sourceWidgetId);
-        if (!sourceMessaging) {
-            console.error("No source widget messaging for temp widget");
-            return;
-        }
-        sourceMessaging.sendTempCloseInfo({success, ...data});
-    }
-
-    private onWidgetClose = (req) => {
-        this.props.onFinished(true);
-        TempWidgetDialog.sendExitData(this.props.sourceWidgetId, true, req.data);
-    }
-
-    private onClientToggleChanged = (androidMode) => {
-        this.setState({androidMode}, () => this.sendTheme());
-    };
-
-    private onDarkThemeChanged = (darkTheme) => {
-        this.setState({darkTheme}, () => this.sendTheme());
-    };
-
-    private onAccentColorChanged = (ev) => {
-        this.setState({accentColor: ev.target.value}, () => this.sendTheme());
-    };
-
-    public render() {
-        // TODO: Don't violate every single security principle
-
-        const widgetUrl = this.props.widgetDefinition.url
-            + "?widgetId=TEMP_ID&parentUrl=" + encodeURIComponent(window.location.href);
-
-        return <BaseDialog
-            title={_t("Widget Proof of Concept Dashboard")}
-            className='mx_TempWidgetDialog'
-            contentId='mx_Dialog_content'
-            onFinished={this.props.onFinished}
-            hasCancel={false}
-        >
-            <div>
-                <LabelledToggleSwitch
-                    label={ _t("Look like Android")}
-                    onChange={this.onClientToggleChanged}
-                    value={this.state.androidMode}
-                />
-                <LabelledToggleSwitch
-                    label={ _t("Look like dark theme")}
-                    onChange={this.onDarkThemeChanged}
-                    value={this.state.darkTheme}
-                />
-                <Field
-                    value={this.state.accentColor}
-                    label={_t('Accent Colour')}
-                    onChange={this.onAccentColorChanged}
-                />
-            </div>
-            <div>
-                <iframe
-                    ref={this.appFrame}
-                    width={700} height={450}
-                    src={widgetUrl}
-                    onLoad={this.onLoad}
-                />
-            </div>
-        </BaseDialog>;
-    }
-}
diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts
new file mode 100644
index 0000000000..4ef94b7231
--- /dev/null
+++ b/src/stores/ModalWidgetStore.ts
@@ -0,0 +1,86 @@
+/*
+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 { AsyncStoreWithClient } from "./AsyncStoreWithClient";
+import defaultDispatcher from "../dispatcher/dispatcher";
+import { ActionPayload } from "../dispatcher/payloads";
+import Modal, {IHandle, IModal} from "../Modal";
+import ModalWidgetDialog from "../components/views/dialogs/ModalWidgetDialog";
+import ActiveWidgetStore from "../stores/ActiveWidgetStore";
+
+interface IState {
+    modal?: IModal<any>;
+    openedFromId?: string;
+}
+
+export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
+    private static internalInstance = new ModalWidgetStore();
+    private modalInstance: IHandle<void[]> = null;
+    private openSourceWidgetId: string = null;
+
+    private constructor() {
+        super(defaultDispatcher, {});
+    }
+
+    public static get instance(): ModalWidgetStore {
+        return ModalWidgetStore.internalInstance;
+    }
+
+    protected async onAction(payload: ActionPayload): Promise<any> {
+        // nothing
+    }
+
+    public canOpenModalWidget = () => {
+        return !this.modalInstance;
+    };
+
+    public openModalWidget = (requestData: any, sourceWidgetId: string) => {
+        if (this.modalInstance) return;
+        this.openSourceWidgetId = sourceWidgetId;
+        this.modalInstance = Modal.createTrackedDialog('Modal Widget', '', ModalWidgetDialog, {
+            widgetDefinition: {...requestData},
+            sourceWidgetId: sourceWidgetId,
+            onFinished: (success: boolean, data?: any) => {
+                if (!success) {
+                    this.closeModalWidget(sourceWidgetId, {
+                        "m.exited": true,
+                    });
+                } else {
+                    this.closeModalWidget(sourceWidgetId, data);
+                }
+
+                this.openSourceWidgetId = null;
+                this.modalInstance = null;
+            },
+        });
+    };
+
+    public closeModalWidget = (sourceWidgetId: string, data?: any) => {
+        if (!this.modalInstance) return;
+        if (this.openSourceWidgetId === sourceWidgetId) {
+            this.openSourceWidgetId = null;
+            this.modalInstance.close();
+            this.modalInstance = null;
+
+            const sourceMessaging = ActiveWidgetStore.getWidgetMessaging(sourceWidgetId);
+            if (!sourceMessaging) {
+                console.error("No source widget messaging for modal widget");
+                return;
+            }
+            sourceMessaging.sendModalCloseInfo(data);
+        }
+    };
+}
diff --git a/src/stores/TempWidgetStore.ts b/src/stores/TempWidgetStore.ts
deleted file mode 100644
index 9f75963fd1..0000000000
--- a/src/stores/TempWidgetStore.ts
+++ /dev/null
@@ -1,54 +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 { AsyncStoreWithClient } from "./AsyncStoreWithClient";
-import defaultDispatcher from "../dispatcher/dispatcher";
-import { ActionPayload } from "../dispatcher/payloads";
-import Modal, { IModal } from "../Modal";
-import TempWidgetDialog from "../components/views/dialogs/TempWidgetDialog";
-
-interface IState {
-    modal?: IModal<any>;
-    openedFromId?: string;
-}
-
-export class TempWidgetStore extends AsyncStoreWithClient<IState> {
-    private static internalInstance = new TempWidgetStore();
-
-    private constructor() {
-        super(defaultDispatcher, {});
-    }
-
-    public static get instance(): TempWidgetStore {
-        return TempWidgetStore.internalInstance;
-    }
-
-    protected async onAction(payload: ActionPayload): Promise<any> {
-        // nothing
-    }
-
-    public openTempWidget(requestData: any, sourceWidgetId: string) {
-        Modal.createTrackedDialog('Temp Widget', '', TempWidgetDialog, {
-            widgetDefinition: {...requestData},
-            sourceWidgetId: sourceWidgetId,
-            onFinished: (success) => {
-                if (!success) {
-                    TempWidgetDialog.sendExitData(sourceWidgetId, false);
-                }
-            },
-        });
-    }
-}
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
index 12bedb04cd..02ec2f4166 100644
--- a/src/widgets/WidgetApi.ts
+++ b/src/widgets/WidgetApi.ts
@@ -25,6 +25,7 @@ export enum Capability {
     Screenshot = "m.capability.screenshot",
     Sticker = "m.sticker",
     AlwaysOnScreen = "m.always_on_screen",
+    Modals = "m.modals",
     ReceiveTerminate = "im.vector.receive_terminate",
 }
 
@@ -39,12 +40,10 @@ export enum KnownWidgetActions {
     SetAlwaysOnScreen = "set_always_on_screen",
     ClientReady = "im.vector.ready",
     Terminate = "im.vector.terminate",
-
-    OpenTempWidget = "io.element.start_temp",
-    UpdateThemeInfo = "io.element.theme_info",
-    SendWidgetConfig = "io.element.widget_config",
-    CloseWidget = "io.element.exit",
-    ClosedWidgetResponse = "io.element.exit_response",
+    OpenModalWidget = "open_modal",
+    CloseModalWidget = "close_modal",
+    GetWidgetConfig = "widget_config",
+    ButtonClicked = "button_clicked",
 }
 
 export type WidgetAction = KnownWidgetActions | string;
@@ -78,6 +77,18 @@ export interface OpenIDCredentials {
     expiresIn: number;
 }
 
+export enum ButtonKind {
+    Primary = "m.primary",
+    Secondary = "m.secondary",
+    Danger = "m.danger",
+}
+
+export interface IButton {
+    id: "m.close" | string;
+    label: string;
+    kind: ButtonKind;
+}
+
 /**
  * Handles Element <--> Widget interactions for embedded/standalone widgets.
  *
@@ -140,9 +151,7 @@ export class WidgetApi extends EventEmitter {
                     // Save OpenID credentials
                     this.setOpenIDCredentials(<ToWidgetRequest>payload);
                     this.replyToRequest(<ToWidgetRequest>payload, {});
-                } else if (payload.action === KnownWidgetActions.UpdateThemeInfo
-                    || payload.action === KnownWidgetActions.SendWidgetConfig
-                    || payload.action === KnownWidgetActions.ClosedWidgetResponse) {
+                } else if (payload.action === KnownWidgetActions.GetWidgetConfig) {
                     // Finalization needs to be async, so postpone with a promise
                     let finalizePromise = Promise.resolve();
                     const wait = (promise) => {
@@ -236,16 +245,16 @@ export class WidgetApi extends EventEmitter {
         });
     }
 
-    public closeWidget(exitData: any): Promise<any> {
+    public closeModalWidget(exitData: any): Promise<any> {
         return new Promise<any>(resolve => {
-            this.callAction(KnownWidgetActions.CloseWidget, exitData, null);
+            this.callAction(KnownWidgetActions.CloseModalWidget, exitData, null);
             resolve();
         });
     }
 
-    public openTempWidget(url: string, data: any): Promise<any> {
+    public openModalWidget(url: string, name: string, buttons: IButton[], data: any): Promise<any> {
         return new Promise<any>(resolve => {
-            this.callAction(KnownWidgetActions.OpenTempWidget, {url, data}, null);
+            this.callAction(KnownWidgetActions.OpenModalWidget, {url, name, buttons, data}, null);
             resolve();
         });
     }

From 6919a8239f0e928f98eaa5290aa8a4f52acf9801 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 25 Sep 2020 14:17:22 +0100
Subject: [PATCH 03/11] i18n

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/i18n/strings/en_EN.json | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 2da00d9317..97bd7a7ea6 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1760,6 +1760,7 @@
     "Verify session": "Verify session",
     "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.",
     "Message edits": "Message edits",
+    "Modal Widget": "Modal Widget",
     "Your account is not secure": "Your account is not secure",
     "Your password": "Your password",
     "This session, or the other session": "This session, or the other session",
@@ -1844,10 +1845,6 @@
     "Missing session data": "Missing session data",
     "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
     "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.",
-    "Widget Proof of Concept Dashboard": "Widget Proof of Concept Dashboard",
-    "Look like Android": "Look like Android",
-    "Look like dark theme": "Look like dark theme",
-    "Accent Colour": "Accent Colour",
     "Integration Manager": "Integration Manager",
     "Find others by phone or email": "Find others by phone or email",
     "Be found by phone or email": "Be found by phone or email",

From e3b0bf19152cbf0aec9d14e2fa2cee82a0ce5f2a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 25 Sep 2020 15:03:54 +0100
Subject: [PATCH 04/11] Fix url encoding issue for modal widgets widgetId

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/@types/global.d.ts                             | 2 ++
 src/components/views/dialogs/ModalWidgetDialog.tsx | 5 +++--
 src/stores/ModalWidgetStore.ts                     | 4 +++-
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index e1111a8a94..75ac1ea31e 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -30,6 +30,7 @@ import {Notifier} from "../Notifier";
 import type {Renderer} from "react-dom";
 import RightPanelStore from "../stores/RightPanelStore";
 import WidgetStore from "../stores/WidgetStore";
+import {ModalWidgetStore} from "../stores/ModalWidgetStore";
 
 declare global {
     interface Window {
@@ -53,6 +54,7 @@ declare global {
         mxNotifier: typeof Notifier;
         mxRightPanelStore: RightPanelStore;
         mxWidgetStore: WidgetStore;
+        mxModalWidgetStore: ModalWidgetStore;
     }
 
     interface Document {
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index d2599d6f90..f17b1ec27a 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -83,8 +83,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
     public render() {
         // TODO: Don't violate every single security principle
 
-        const widgetUrl = this.props.widgetDefinition.url
-            + `?widgetId=${this.getWidgetId()}&parentUrl=${encodeURIComponent(window.location.href)}`;
+        const widgetId = encodeURIComponent(this.getWidgetId());
+        const parentUrl = encodeURIComponent(window.location.href);
+        const widgetUrl = `${this.props.widgetDefinition.url}?widgetId=${widgetId}&parentUrl=${parentUrl}`;
 
         let buttons;
         if (this.props.widgetDefinition.buttons) {
diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts
index 4ef94b7231..8caebf549b 100644
--- a/src/stores/ModalWidgetStore.ts
+++ b/src/stores/ModalWidgetStore.ts
@@ -52,7 +52,7 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
         this.openSourceWidgetId = sourceWidgetId;
         this.modalInstance = Modal.createTrackedDialog('Modal Widget', '', ModalWidgetDialog, {
             widgetDefinition: {...requestData},
-            sourceWidgetId: sourceWidgetId,
+            sourceWidgetId,
             onFinished: (success: boolean, data?: any) => {
                 if (!success) {
                     this.closeModalWidget(sourceWidgetId, {
@@ -84,3 +84,5 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
         }
     };
 }
+
+window.mxModalWidgetStore = ModalWidgetStore.instance;

From 30d2e61a0dc3665698da03d4368c20d2936190b5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 28 Sep 2020 10:50:31 +0100
Subject: [PATCH 05/11] Continuation

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/FromWidgetPostMessageApi.js               |  1 +
 .../views/dialogs/ModalWidgetDialog.tsx       | 12 +--
 src/widgets/WidgetApi.ts                      | 73 ++++++++++---------
 3 files changed, 46 insertions(+), 40 deletions(-)

diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
index f41cf0543d..00e5588ec0 100644
--- a/src/FromWidgetPostMessageApi.js
+++ b/src/FromWidgetPostMessageApi.js
@@ -259,6 +259,7 @@ export default class FromWidgetPostMessageApi {
     sendResponse(event, res) {
         const data = objectClone(event.data);
         data.response = res;
+        if (!event.source) return; // source may have gone away since
         event.source.postMessage(data, event.origin);
     }
 
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index f17b1ec27a..60f5a09666 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -83,9 +83,11 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
     public render() {
         // TODO: Don't violate every single security principle
 
-        const widgetId = encodeURIComponent(this.getWidgetId());
-        const parentUrl = encodeURIComponent(window.location.href);
-        const widgetUrl = `${this.props.widgetDefinition.url}?widgetId=${widgetId}&parentUrl=${parentUrl}`;
+        const widgetUrl = new URL(this.props.widgetDefinition.url);
+        // TODO: Replace these with proper widget params
+        // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
+        widgetUrl.searchParams.set("widgetId", this.getWidgetId());
+        widgetUrl.searchParams.set("parentUrl", window.location.href);
 
         let buttons;
         if (this.props.widgetDefinition.buttons) {
@@ -124,10 +126,10 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
             <div>
                 <iframe
                     ref={this.appFrame}
-                    sandbox="allow-forms allow-scripts"
+                    // sandbox="allow-forms allow-scripts"
                     width={700} // TODO
                     height={450} // TODO
-                    src={widgetUrl}
+                    src={widgetUrl.toString()}
                     onLoad={this.onLoad}
                 />
             </div>
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
index 02ec2f4166..6ce4f0e47d 100644
--- a/src/widgets/WidgetApi.ts
+++ b/src/widgets/WidgetApi.ts
@@ -126,44 +126,47 @@ export class WidgetApi extends EventEmitter {
             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();
+                switch (payload.action) {
+                    case KnownWidgetActions.GetCapabilities:
+                        this.onCapabilitiesRequest(<ToWidgetRequest>payload);
+                        if (!this.expectingExplicitReady) {
+                            this.readyPromiseResolve();
+                        }
+                        break;
 
-                    // Automatically acknowledge so we can move on
-                    this.replyToRequest(<ToWidgetRequest>payload, {});
-                } else if (payload.action === KnownWidgetActions.Terminate) {
-                    // Finalization needs to be async, so postpone with a promise
-                    let finalizePromise = Promise.resolve();
-                    const wait = (promise) => {
-                        finalizePromise = finalizePromise.then(() => promise);
-                    };
-                    this.emit('terminate', wait);
-                    Promise.resolve(finalizePromise).then(() => {
-                        // Acknowledge that we're shut down now
+                    case KnownWidgetActions.ClientReady:
+                        this.readyPromiseResolve();
+
+                        // Automatically acknowledge so we can move on
                         this.replyToRequest(<ToWidgetRequest>payload, {});
-                    });
-                } else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) {
-                    // Save OpenID credentials
-                    this.setOpenIDCredentials(<ToWidgetRequest>payload);
-                    this.replyToRequest(<ToWidgetRequest>payload, {});
-                } else if (payload.action === KnownWidgetActions.GetWidgetConfig) {
-                    // Finalization needs to be async, so postpone with a promise
-                    let finalizePromise = Promise.resolve();
-                    const wait = (promise) => {
-                        finalizePromise = finalizePromise.then(() => promise);
-                    };
-                    this.emit(payload.action, payload, wait);
-                    Promise.resolve(finalizePromise).then(() => {
-                        // Acknowledge that we're shut down now
+                        break;
+
+                    case KnownWidgetActions.ReceiveOpenIDCredentials:
+                        // Save OpenID credentials
+                        this.setOpenIDCredentials(<ToWidgetRequest>payload);
                         this.replyToRequest(<ToWidgetRequest>payload, {});
-                    });
-                } else {
-                    console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
+                        break;
+
+                    // Ack, handle by caller
+                    case KnownWidgetActions.Terminate:
+                    case KnownWidgetActions.ButtonClicked:
+                    case KnownWidgetActions.GetWidgetConfig:
+                    case KnownWidgetActions.CloseModalWidget: {
+                        // Finalization needs to be async, so postpone with a promise
+                        let finalizePromise = Promise.resolve();
+                        const wait = (promise) => {
+                            finalizePromise = finalizePromise.then(() => promise);
+                        };
+                        this.emit(payload.action, payload, wait);
+                        Promise.resolve(finalizePromise).then(() => {
+                            // Acknowledge that we're shut down now
+                            this.replyToRequest(<ToWidgetRequest>payload, {});
+                        });
+                        break;
+                    }
+
+                    default:
+                        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)}`);

From 13d09df7d79d0d810eb010d0ddfdb6f8e5310aa9 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 28 Sep 2020 11:59:39 +0100
Subject: [PATCH 06/11] tidy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 res/css/views/dialogs/_ModalWidgetDialog.scss      | 5 +++++
 src/components/views/dialogs/ModalWidgetDialog.tsx | 5 +----
 src/widgets/WidgetApi.ts                           | 2 +-
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/res/css/views/dialogs/_ModalWidgetDialog.scss b/res/css/views/dialogs/_ModalWidgetDialog.scss
index f0eb19edb3..919a98723e 100644
--- a/res/css/views/dialogs/_ModalWidgetDialog.scss
+++ b/res/css/views/dialogs/_ModalWidgetDialog.scss
@@ -20,4 +20,9 @@ limitations under the License.
             margin-left: 8px;
         }
     }
+
+    iframe {
+        width: 100%;
+        height: 450px;
+    }
 }
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index 60f5a09666..f4226522ee 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -121,14 +121,11 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
             className="mx_ModalWidgetDialog"
             contentId="mx_Dialog_content"
             onFinished={this.props.onFinished}
-            hasCancel={false}
         >
             <div>
                 <iframe
                     ref={this.appFrame}
-                    // sandbox="allow-forms allow-scripts"
-                    width={700} // TODO
-                    height={450} // TODO
+                    // sandbox="allow-forms allow-scripts" TODO
                     src={widgetUrl.toString()}
                     onLoad={this.onLoad}
                 />
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
index 6ce4f0e47d..15607f6907 100644
--- a/src/widgets/WidgetApi.ts
+++ b/src/widgets/WidgetApi.ts
@@ -147,7 +147,7 @@ export class WidgetApi extends EventEmitter {
                         this.replyToRequest(<ToWidgetRequest>payload, {});
                         break;
 
-                    // Ack, handle by caller
+                    // Ack, handled by caller
                     case KnownWidgetActions.Terminate:
                     case KnownWidgetActions.ButtonClicked:
                     case KnownWidgetActions.GetWidgetConfig:

From 16362440b318dfe7d5a081b15adf6c2e79b0ac35 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 19 Oct 2020 20:39:43 +0100
Subject: [PATCH 07/11] Iterate Modal Widgets

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../views/dialogs/ModalWidgetDialog.tsx       | 98 +++++++++++--------
 src/stores/ModalWidgetStore.ts                | 25 +++--
 src/stores/widgets/StopGapWidget.ts           | 19 +++-
 3 files changed, 88 insertions(+), 54 deletions(-)

diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index f4226522ee..e9fd30c1c3 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -17,77 +17,95 @@ limitations under the License.
 import * as React from 'react';
 import BaseDialog from './BaseDialog';
 import { _t } from '../../../languageHandler';
-import WidgetMessaging from "../../../WidgetMessaging";
-import {ButtonKind, IButton, KnownWidgetActions} from "../../../widgets/WidgetApi";
 import AccessibleButton from "../elements/AccessibleButton";
-
-interface IModalWidget {
-    type: string;
-    url: string;
-    name: string;
-    data: any;
-    waitForIframeLoad?: boolean;
-    buttons?: IButton[];
-}
+import {
+    ClientWidgetApi,
+    IModalWidgetCloseRequest,
+    IModalWidgetOpenRequestData,
+    IModalWidgetReturnData,
+    ModalButtonKind,
+    Widget,
+    WidgetApiFromWidgetAction,
+} from "matrix-widget-api";
+import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
+import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import RoomViewStore from "../../../stores/RoomViewStore";
+import {OwnProfileStore} from "../../../stores/OwnProfileStore";
 
 interface IProps {
-    widgetDefinition: IModalWidget;
+    widgetDefinition: IModalWidgetOpenRequestData;
     sourceWidgetId: string;
-    onFinished(success: boolean, data?: any): void;
+    onFinished(success: boolean, data?: IModalWidgetReturnData): void;
 }
 
 interface IState {
-    messaging?: WidgetMessaging;
+    messaging?: ClientWidgetApi;
 }
 
 const MAX_BUTTONS = 3;
 
 export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
+    private readonly widget: Widget;
     private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
 
     state: IState = {};
 
-    private getWidgetId() {
-        return `modal_${this.props.sourceWidgetId}`;
+    constructor(props) {
+        super(props);
+
+        this.widget = new Widget({
+            ...this.props.widgetDefinition,
+            creatorUserId: MatrixClientPeg.get().getUserId(),
+            id: `modal_${this.props.sourceWidgetId}`,
+        });
     }
 
     public componentDidMount() {
-        // TODO: Don't violate every principle of widget creation
-        const messaging = new WidgetMessaging(
-            this.getWidgetId(),
-            this.props.widgetDefinition.url,
-            this.props.widgetDefinition.url, // TODO templating and such
-            true,
-            this.appFrame.current.contentWindow,
-        );
+        const driver = new StopGapWidgetDriver( []);
+        const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
         this.setState({messaging});
     }
 
     public componentWillUnmount() {
-        this.state.messaging.fromWidget.removeListener(KnownWidgetActions.CloseModalWidget, this.onWidgetClose);
+        this.state.messaging.off("ready", this.onReady);
+        this.state.messaging.off(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
         this.state.messaging.stop();
     }
 
-    private onLoad = () => {
-        this.state.messaging.getCapabilities().then(caps => {
-            console.log("Requested capabilities: ", caps);
-            this.state.messaging.sendWidgetConfig(this.props.widgetDefinition.data);
-        });
-        this.state.messaging.fromWidget.addListener(KnownWidgetActions.CloseModalWidget, this.onWidgetClose);
+    private onReady = () => {
+        this.state.messaging.sendWidgetConfig(this.props.widgetDefinition);
     };
 
-    private onWidgetClose = (req) => {
-        this.props.onFinished(true, req.data);
+    private onLoad = () => {
+        this.state.messaging.once("ready", this.onReady);
+        this.state.messaging.on(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
+    };
+
+    private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>) => {
+        this.props.onFinished(true, ev.detail.data);
     }
 
     public render() {
         // TODO: Don't violate every single security principle
+        // TODO copied from SGWidget
+        const templated = this.widget.getCompleteUrl({
+            currentRoomId: RoomViewStore.getRoomId(),
+            currentUserId: MatrixClientPeg.get().getUserId(),
+            userDisplayName: OwnProfileStore.instance.displayName,
+            userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
+        });
 
-        const widgetUrl = new URL(this.props.widgetDefinition.url);
+        const parsed = new URL(templated);
+
+        // Add in some legacy support sprinkles (for non-popout widgets)
         // TODO: Replace these with proper widget params
         // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
-        widgetUrl.searchParams.set("widgetId", this.getWidgetId());
-        widgetUrl.searchParams.set("parentUrl", window.location.href);
+        parsed.searchParams.set('widgetId', this.widget.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.
+        const widgetUrl = parsed.toString().replace(/%24/g, '$');
 
         let buttons;
         if (this.props.widgetDefinition.buttons) {
@@ -95,19 +113,19 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
             buttons = this.props.widgetDefinition.buttons.slice(0, MAX_BUTTONS).reverse().map(def => {
                 let kind = "secondary";
                 switch (def.kind) {
-                    case ButtonKind.Primary:
+                    case ModalButtonKind.Primary:
                         kind = "primary";
                         break;
-                    case ButtonKind.Secondary:
+                    case ModalButtonKind.Secondary:
                         kind = "primary_outline";
                         break
-                    case ButtonKind.Danger:
+                    case ModalButtonKind.Danger:
                         kind = "danger";
                         break;
                 }
 
                 const onClick = () => {
-                    this.state.messaging.sendModalButtonClicked(def.id);
+                    this.state.messaging.notifyModalWidgetButtonClicked(def.id);
                 };
 
                 return <AccessibleButton key={def.id} kind={kind} onClick={onClick}>
diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts
index 8caebf549b..0485afd106 100644
--- a/src/stores/ModalWidgetStore.ts
+++ b/src/stores/ModalWidgetStore.ts
@@ -19,7 +19,8 @@ import defaultDispatcher from "../dispatcher/dispatcher";
 import { ActionPayload } from "../dispatcher/payloads";
 import Modal, {IHandle, IModal} from "../Modal";
 import ModalWidgetDialog from "../components/views/dialogs/ModalWidgetDialog";
-import ActiveWidgetStore from "../stores/ActiveWidgetStore";
+import {WidgetMessagingStore} from "./widgets/WidgetMessagingStore";
+import {IModalWidgetOpenRequestData, IModalWidgetReturnData, Widget} from "matrix-widget-api";
 
 interface IState {
     modal?: IModal<any>;
@@ -47,19 +48,17 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
         return !this.modalInstance;
     };
 
-    public openModalWidget = (requestData: any, sourceWidgetId: string) => {
+    public openModalWidget = (requestData: IModalWidgetOpenRequestData, sourceWidget: Widget) => {
         if (this.modalInstance) return;
-        this.openSourceWidgetId = sourceWidgetId;
+        this.openSourceWidgetId = sourceWidget.id;
         this.modalInstance = Modal.createTrackedDialog('Modal Widget', '', ModalWidgetDialog, {
             widgetDefinition: {...requestData},
-            sourceWidgetId,
-            onFinished: (success: boolean, data?: any) => {
+            sourceWidgetId: sourceWidget.id,
+            onFinished: (success: boolean, data?: IModalWidgetReturnData) => {
                 if (!success) {
-                    this.closeModalWidget(sourceWidgetId, {
-                        "m.exited": true,
-                    });
+                    this.closeModalWidget(sourceWidget, { "m.exited": true });
                 } else {
-                    this.closeModalWidget(sourceWidgetId, data);
+                    this.closeModalWidget(sourceWidget, data);
                 }
 
                 this.openSourceWidgetId = null;
@@ -68,19 +67,19 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
         });
     };
 
-    public closeModalWidget = (sourceWidgetId: string, data?: any) => {
+    public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => {
         if (!this.modalInstance) return;
-        if (this.openSourceWidgetId === sourceWidgetId) {
+        if (this.openSourceWidgetId === sourceWidget.id) {
             this.openSourceWidgetId = null;
             this.modalInstance.close();
             this.modalInstance = null;
 
-            const sourceMessaging = ActiveWidgetStore.getWidgetMessaging(sourceWidgetId);
+            const sourceMessaging = WidgetMessagingStore.instance.getMessaging(sourceWidget);
             if (!sourceMessaging) {
                 console.error("No source widget messaging for modal widget");
                 return;
             }
-            sourceMessaging.sendModalCloseInfo(data);
+            sourceMessaging.notifyModalWidgetClose(data);
         }
     };
 }
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 17302d0ab9..0299f74cb8 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -32,6 +32,7 @@ import {
     Widget,
     WidgetApiToWidgetAction,
     WidgetApiFromWidgetAction,
+    IModalWidgetOpenRequest,
 } from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";
@@ -49,6 +50,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
 import { ElementWidgetActions } from "./ElementWidgetActions";
 import Modal from "../../Modal";
 import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
+import {ModalWidgetStore} from "../ModalWidgetStore";
 
 // TODO: Destroy all of this code
 
@@ -201,7 +203,7 @@ export class StopGapWidget extends EventEmitter {
     }
 
     private onOpenIdReq = async (ev: CustomEvent<IGetOpenIDActionRequest>) => {
-        if (ev?.detail?.widgetId !== this.widgetId) return;
+        ev.preventDefault();
 
         const rawUrl = this.appTileProps.app.url;
         const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, rawUrl, this.appTileProps.userWidget);
@@ -249,6 +251,20 @@ export class StopGapWidget extends EventEmitter {
         });
     };
 
+    private onOpenModal = async (ev: CustomEvent<IModalWidgetOpenRequest>) => {
+        ev.preventDefault();
+        if (ModalWidgetStore.instance.canOpenModalWidget()) {
+            ModalWidgetStore.instance.openModalWidget(ev.detail.data, this.mockWidget);
+            this.messaging.transport.reply(ev.detail, {}); // ack
+        } else {
+            this.messaging.transport.reply(ev.detail, {
+                error: {
+                    message: "Unable to open modal at this time",
+                },
+            })
+        }
+    };
+
     public start(iframe: HTMLIFrameElement) {
         if (this.started) return;
         const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
@@ -256,6 +272,7 @@ export class StopGapWidget extends EventEmitter {
         this.messaging.on("preparing", () => this.emit("preparing"));
         this.messaging.on("ready", () => this.emit("ready"));
         this.messaging.on(`action:${WidgetApiFromWidgetAction.GetOpenIDCredentials}`, this.onOpenIdReq);
+        this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal);
         WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
 
         if (!this.appTileProps.userWidget && this.appTileProps.room) {

From b6fd2a0691ce92f82fc9ee810f80e0295afdfefb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 20 Oct 2020 15:05:22 +0100
Subject: [PATCH 08/11] Iterate the Modal Widget Dialog paddings and phishing
 warning

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 res/css/views/dialogs/_ModalWidgetDialog.scss | 14 ++++++++++++++
 res/img/element-icons/warning-badge.svg       |  5 +++++
 .../views/dialogs/ModalWidgetDialog.tsx       | 19 +++++++++++++++----
 src/i18n/strings/en_EN.json                   |  1 +
 4 files changed, 35 insertions(+), 4 deletions(-)
 create mode 100644 res/img/element-icons/warning-badge.svg

diff --git a/res/css/views/dialogs/_ModalWidgetDialog.scss b/res/css/views/dialogs/_ModalWidgetDialog.scss
index 919a98723e..aa2dd0d395 100644
--- a/res/css/views/dialogs/_ModalWidgetDialog.scss
+++ b/res/css/views/dialogs/_ModalWidgetDialog.scss
@@ -15,7 +15,19 @@ limitations under the License.
 */
 
 .mx_ModalWidgetDialog {
+    .mx_ModalWidgetDialog_warning {
+        margin-bottom: 24px;
+
+        > img {
+            vertical-align: middle;
+            margin-right: 8px;
+        }
+    }
+
     .mx_ModalWidgetDialog_buttons {
+        float: right;
+        margin-top: 24px;
+
         .mx_AccessibleButton + .mx_AccessibleButton {
             margin-left: 8px;
         }
@@ -24,5 +36,7 @@ limitations under the License.
     iframe {
         width: 100%;
         height: 450px;
+        border: 0;
+        border-radius: 8px;
     }
 }
diff --git a/res/img/element-icons/warning-badge.svg b/res/img/element-icons/warning-badge.svg
new file mode 100644
index 0000000000..ac5991f221
--- /dev/null
+++ b/res/img/element-icons/warning-badge.svg
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="8" cy="8" r="8" fill="#737D8C" style="mix-blend-mode:multiply"/>
+<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
+<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
+</svg>
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index e9fd30c1c3..d7f6e96394 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -87,7 +87,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
 
     public render() {
         // TODO: Don't violate every single security principle
-        // TODO copied from SGWidget
+        // TODO: DRY - copied from SGWidget
         const templated = this.widget.getCompleteUrl({
             currentRoomId: RoomViewStore.getRoomId(),
             currentUserId: MatrixClientPeg.get().getUserId(),
@@ -140,15 +140,26 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
             contentId="mx_Dialog_content"
             onFinished={this.props.onFinished}
         >
+            <div className="mx_ModalWidgetDialog_warning">
+                <img
+                    src={require("../../../../res/img/element-icons/warning-badge.svg")}
+                    height="16"
+                    width="16"
+                    alt=""
+                />
+                {_t("Data on this screen is shared with %(widgetDomain)s", {
+                    widgetDomain: parsed.hostname,
+                })}
+            </div>
             <div>
                 <iframe
                     ref={this.appFrame}
-                    // sandbox="allow-forms allow-scripts" TODO
-                    src={widgetUrl.toString()}
+                    sandbox="allow-forms allow-scripts"
+                    src={widgetUrl}
                     onLoad={this.onLoad}
                 />
             </div>
-            <div className="mx_ModalWidgetDialog_buttons" style={{float: "right"}}>
+            <div className="mx_ModalWidgetDialog_buttons">
                 { buttons }
             </div>
         </BaseDialog>;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index c984c27dba..1786fb20b3 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1769,6 +1769,7 @@
     "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.",
     "Message edits": "Message edits",
     "Modal Widget": "Modal Widget",
+    "Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s",
     "Your account is not secure": "Your account is not secure",
     "Your password": "Your password",
     "This session, or the other session": "This session, or the other session",

From 24c4a3876c1e60d5321555e820f0e415db4b9026 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 20 Oct 2020 15:05:38 +0100
Subject: [PATCH 09/11] Increase Border Radius of Accessible Buttons to 8px

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 res/css/views/elements/_AccessibleButton.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss
index 96269cea43..9c26f8f120 100644
--- a/res/css/views/elements/_AccessibleButton.scss
+++ b/res/css/views/elements/_AccessibleButton.scss
@@ -25,7 +25,7 @@ limitations under the License.
 .mx_AccessibleButton_hasKind {
     padding: 7px 18px;
     text-align: center;
-    border-radius: 4px;
+    border-radius: 8px;
     display: inline-block;
     font-size: $font-14px;
 }

From 12bc3ea9dc16d87671fac66be6b7835fa8a3e713 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 22 Oct 2020 21:34:48 +0100
Subject: [PATCH 10/11] remove stale comments

---
 src/components/views/dialogs/ModalWidgetDialog.tsx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index d7f6e96394..52d708a4c4 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -86,8 +86,6 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
     }
 
     public render() {
-        // TODO: Don't violate every single security principle
-        // TODO: DRY - copied from SGWidget
         const templated = this.widget.getCompleteUrl({
             currentRoomId: RoomViewStore.getRoomId(),
             currentUserId: MatrixClientPeg.get().getUserId(),

From cf93f75d09f27e7025e85d09eb2ed0c5e77f6816 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 22 Oct 2020 22:09:16 +0100
Subject: [PATCH 11/11] fix sandbox flags

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

diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index 52d708a4c4..6ce3230a7a 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -152,7 +152,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
             <div>
                 <iframe
                     ref={this.appFrame}
-                    sandbox="allow-forms allow-scripts"
+                    sandbox="allow-forms allow-scripts allow-same-origin"
                     src={widgetUrl}
                     onLoad={this.onLoad}
                 />