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 { close(...args: T): void; } -interface IHandle { +export interface IHandle { finished: Promise; 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 { + private appFrame: React.RefObject = 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 + { def.label } + ; + }); + } + + return +
+