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] 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 { + private readonly widget: Widget; private appFrame: React.RefObject = 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) => { + 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 { 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 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; @@ -47,19 +48,17 @@ export class ModalWidgetStore extends AsyncStoreWithClient { 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 { }); }; - 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) => { - 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) => { + 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) {