diff --git a/res/css/_components.scss b/res/css/_components.scss index ad3cfbdcea7..657d77974ff 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -75,6 +75,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 00000000000..aa2dd0d3958 --- /dev/null +++ b/res/css/views/dialogs/_ModalWidgetDialog.scss @@ -0,0 +1,42 @@ +/* +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_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; + } + } + + iframe { + width: 100%; + height: 450px; + border: 0; + border-radius: 8px; + } +} diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 96269cea433..9c26f8f1206 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; } diff --git a/res/img/element-icons/warning-badge.svg b/res/img/element-icons/warning-badge.svg new file mode 100644 index 00000000000..ac5991f2212 --- /dev/null +++ b/res/img/element-icons/warning-badge.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index ed28a5c4791..161aa3797f7 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -34,6 +34,7 @@ import WidgetStore from "../stores/WidgetStore"; import CallHandler from "../CallHandler"; import {Analytics} from "../Analytics"; import UserActivity from "../UserActivity"; +import {ModalWidgetStore} from "../stores/ModalWidgetStore"; declare global { interface Window { @@ -60,6 +61,7 @@ declare global { mxCallHandler: CallHandler; mxAnalytics: Analytics; mxUserActivity: UserActivity; + mxModalWidgetStore: ModalWidgetStore; } interface Document { diff --git a/src/Modal.tsx b/src/Modal.tsx index b0f6ef988e5..2f761e73937 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 { +export interface IModal { elem: React.ReactNode; className?: string; beforeClosePromise?: Promise; @@ -38,7 +38,7 @@ interface IModal { close(...args: T): void; } -interface IHandle { +export interface IHandle { finished: Promise; close(...args: T): void; } diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx new file mode 100644 index 00000000000..6ce3230a7aa --- /dev/null +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -0,0 +1,165 @@ +/* +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 AccessibleButton from "../elements/AccessibleButton"; +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: IModalWidgetOpenRequestData; + sourceWidgetId: string; + onFinished(success: boolean, data?: IModalWidgetReturnData): void; +} + +interface IState { + 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 = {}; + + constructor(props) { + super(props); + + this.widget = new Widget({ + ...this.props.widgetDefinition, + creatorUserId: MatrixClientPeg.get().getUserId(), + id: `modal_${this.props.sourceWidgetId}`, + }); + } + + public componentDidMount() { + const driver = new StopGapWidgetDriver( []); + const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver); + this.setState({messaging}); + } + + public componentWillUnmount() { + this.state.messaging.off("ready", this.onReady); + this.state.messaging.off(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose); + this.state.messaging.stop(); + } + + private onReady = () => { + this.state.messaging.sendWidgetConfig(this.props.widgetDefinition); + }; + + 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() { + const templated = this.widget.getCompleteUrl({ + currentRoomId: RoomViewStore.getRoomId(), + currentUserId: MatrixClientPeg.get().getUserId(), + userDisplayName: OwnProfileStore.instance.displayName, + userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(), + }); + + 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 + 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) { + // 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 ModalButtonKind.Primary: + kind = "primary"; + break; + case ModalButtonKind.Secondary: + kind = "primary_outline"; + break + case ModalButtonKind.Danger: + kind = "danger"; + break; + } + + const onClick = () => { + this.state.messaging.notifyModalWidgetButtonClicked(def.id); + }; + + return + { def.label } + ; + }); + } + + return +
+ + {_t("Data on this screen is shared with %(widgetDomain)s", { + widgetDomain: parsed.hostname, + })} +
+
+