Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Modal Widgets - MSC2790 #5252

Merged
merged 15 commits into from
Oct 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
42 changes: 42 additions & 0 deletions res/css/views/dialogs/_ModalWidgetDialog.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion res/css/views/elements/_AccessibleButton.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions res/img/element-icons/warning-badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -60,6 +61,7 @@ declare global {
mxCallHandler: CallHandler;
mxAnalytics: Analytics;
mxUserActivity: UserActivity;
mxModalWidgetStore: ModalWidgetStore;
}

interface Document {
Expand Down
4 changes: 2 additions & 2 deletions src/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -38,7 +38,7 @@ 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;
}
Expand Down
165 changes: 165 additions & 0 deletions src/components/views/dialogs/ModalWidgetDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<IProps, IState> {
private readonly widget: Widget;
private appFrame: React.RefObject<HTMLIFrameElement> = 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<IModalWidgetCloseRequest>) => {
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]);
Comment on lines +98 to +102
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly for my notes: a lot of this is copied from SGWidget, but that's going away with the stuff I'm doing. Will need to refactor this a bit.


// 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 <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}
>
<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 allow-same-origin"
src={widgetUrl}
onLoad={this.onLoad}
/>
</div>
<div className="mx_ModalWidgetDialog_buttons">
{ buttons }
</div>
</BaseDialog>;
}
}
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,8 @@
"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",
"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",
Expand Down
87 changes: 87 additions & 0 deletions src/stores/ModalWidgetStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
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 {WidgetMessagingStore} from "./widgets/WidgetMessagingStore";
import {IModalWidgetOpenRequestData, IModalWidgetReturnData, Widget} from "matrix-widget-api";

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: IModalWidgetOpenRequestData, sourceWidget: Widget) => {
if (this.modalInstance) return;
this.openSourceWidgetId = sourceWidget.id;
this.modalInstance = Modal.createTrackedDialog('Modal Widget', '', ModalWidgetDialog, {
widgetDefinition: {...requestData},
sourceWidgetId: sourceWidget.id,
onFinished: (success: boolean, data?: IModalWidgetReturnData) => {
if (!success) {
this.closeModalWidget(sourceWidget, { "m.exited": true });
} else {
this.closeModalWidget(sourceWidget, data);
}

this.openSourceWidgetId = null;
this.modalInstance = null;
},
});
};

public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => {
if (!this.modalInstance) return;
if (this.openSourceWidgetId === sourceWidget.id) {
this.openSourceWidgetId = null;
this.modalInstance.close();
this.modalInstance = null;

const sourceMessaging = WidgetMessagingStore.instance.getMessaging(sourceWidget);
if (!sourceMessaging) {
console.error("No source widget messaging for modal widget");
return;
}
sourceMessaging.notifyModalWidgetClose(data);
}
};
}

window.mxModalWidgetStore = ModalWidgetStore.instance;
Loading