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

Commit 646ed4c

Browse files
authored
Merge pull request #5252 from matrix-org/t3chguy/feat/modal-widgets
Modal Widgets - MSC2790
2 parents ca4e720 + cf93f75 commit 646ed4c

File tree

10 files changed

+325
-4
lines changed

10 files changed

+325
-4
lines changed

res/css/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
@import "./views/dialogs/_InviteDialog.scss";
7676
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
7777
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
78+
@import "./views/dialogs/_ModalWidgetDialog.scss";
7879
@import "./views/dialogs/_NewSessionReviewDialog.scss";
7980
@import "./views/dialogs/_RoomSettingsDialog.scss";
8081
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_ModalWidgetDialog {
18+
.mx_ModalWidgetDialog_warning {
19+
margin-bottom: 24px;
20+
21+
> img {
22+
vertical-align: middle;
23+
margin-right: 8px;
24+
}
25+
}
26+
27+
.mx_ModalWidgetDialog_buttons {
28+
float: right;
29+
margin-top: 24px;
30+
31+
.mx_AccessibleButton + .mx_AccessibleButton {
32+
margin-left: 8px;
33+
}
34+
}
35+
36+
iframe {
37+
width: 100%;
38+
height: 450px;
39+
border: 0;
40+
border-radius: 8px;
41+
}
42+
}

res/css/views/elements/_AccessibleButton.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ limitations under the License.
2525
.mx_AccessibleButton_hasKind {
2626
padding: 7px 18px;
2727
text-align: center;
28-
border-radius: 4px;
28+
border-radius: 8px;
2929
display: inline-block;
3030
font-size: $font-14px;
3131
}
Lines changed: 5 additions & 0 deletions
Loading

src/@types/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import WidgetStore from "../stores/WidgetStore";
3434
import CallHandler from "../CallHandler";
3535
import {Analytics} from "../Analytics";
3636
import UserActivity from "../UserActivity";
37+
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
3738

3839
declare global {
3940
interface Window {
@@ -60,6 +61,7 @@ declare global {
6061
mxCallHandler: CallHandler;
6162
mxAnalytics: Analytics;
6263
mxUserActivity: UserActivity;
64+
mxModalWidgetStore: ModalWidgetStore;
6365
}
6466

6567
interface Document {

src/Modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper';
2828
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
2929
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
3030

31-
interface IModal<T extends any[]> {
31+
export interface IModal<T extends any[]> {
3232
elem: React.ReactNode;
3333
className?: string;
3434
beforeClosePromise?: Promise<boolean>;
@@ -38,7 +38,7 @@ interface IModal<T extends any[]> {
3838
close(...args: T): void;
3939
}
4040

41-
interface IHandle<T extends any[]> {
41+
export interface IHandle<T extends any[]> {
4242
finished: Promise<T>;
4343
close(...args: T): void;
4444
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import * as React from 'react';
18+
import BaseDialog from './BaseDialog';
19+
import { _t } from '../../../languageHandler';
20+
import AccessibleButton from "../elements/AccessibleButton";
21+
import {
22+
ClientWidgetApi,
23+
IModalWidgetCloseRequest,
24+
IModalWidgetOpenRequestData,
25+
IModalWidgetReturnData,
26+
ModalButtonKind,
27+
Widget,
28+
WidgetApiFromWidgetAction,
29+
} from "matrix-widget-api";
30+
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
31+
import {MatrixClientPeg} from "../../../MatrixClientPeg";
32+
import RoomViewStore from "../../../stores/RoomViewStore";
33+
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
34+
35+
interface IProps {
36+
widgetDefinition: IModalWidgetOpenRequestData;
37+
sourceWidgetId: string;
38+
onFinished(success: boolean, data?: IModalWidgetReturnData): void;
39+
}
40+
41+
interface IState {
42+
messaging?: ClientWidgetApi;
43+
}
44+
45+
const MAX_BUTTONS = 3;
46+
47+
export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
48+
private readonly widget: Widget;
49+
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
50+
51+
state: IState = {};
52+
53+
constructor(props) {
54+
super(props);
55+
56+
this.widget = new Widget({
57+
...this.props.widgetDefinition,
58+
creatorUserId: MatrixClientPeg.get().getUserId(),
59+
id: `modal_${this.props.sourceWidgetId}`,
60+
});
61+
}
62+
63+
public componentDidMount() {
64+
const driver = new StopGapWidgetDriver( []);
65+
const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
66+
this.setState({messaging});
67+
}
68+
69+
public componentWillUnmount() {
70+
this.state.messaging.off("ready", this.onReady);
71+
this.state.messaging.off(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
72+
this.state.messaging.stop();
73+
}
74+
75+
private onReady = () => {
76+
this.state.messaging.sendWidgetConfig(this.props.widgetDefinition);
77+
};
78+
79+
private onLoad = () => {
80+
this.state.messaging.once("ready", this.onReady);
81+
this.state.messaging.on(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose);
82+
};
83+
84+
private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>) => {
85+
this.props.onFinished(true, ev.detail.data);
86+
}
87+
88+
public render() {
89+
const templated = this.widget.getCompleteUrl({
90+
currentRoomId: RoomViewStore.getRoomId(),
91+
currentUserId: MatrixClientPeg.get().getUserId(),
92+
userDisplayName: OwnProfileStore.instance.displayName,
93+
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
94+
});
95+
96+
const parsed = new URL(templated);
97+
98+
// Add in some legacy support sprinkles (for non-popout widgets)
99+
// TODO: Replace these with proper widget params
100+
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
101+
parsed.searchParams.set('widgetId', this.widget.id);
102+
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
103+
104+
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
105+
// in HTTP, but URL parsers encode them anyways.
106+
const widgetUrl = parsed.toString().replace(/%24/g, '$');
107+
108+
let buttons;
109+
if (this.props.widgetDefinition.buttons) {
110+
// show first button rightmost for a more natural specification
111+
buttons = this.props.widgetDefinition.buttons.slice(0, MAX_BUTTONS).reverse().map(def => {
112+
let kind = "secondary";
113+
switch (def.kind) {
114+
case ModalButtonKind.Primary:
115+
kind = "primary";
116+
break;
117+
case ModalButtonKind.Secondary:
118+
kind = "primary_outline";
119+
break
120+
case ModalButtonKind.Danger:
121+
kind = "danger";
122+
break;
123+
}
124+
125+
const onClick = () => {
126+
this.state.messaging.notifyModalWidgetButtonClicked(def.id);
127+
};
128+
129+
return <AccessibleButton key={def.id} kind={kind} onClick={onClick}>
130+
{ def.label }
131+
</AccessibleButton>;
132+
});
133+
}
134+
135+
return <BaseDialog
136+
title={this.props.widgetDefinition.name || _t("Modal Widget")}
137+
className="mx_ModalWidgetDialog"
138+
contentId="mx_Dialog_content"
139+
onFinished={this.props.onFinished}
140+
>
141+
<div className="mx_ModalWidgetDialog_warning">
142+
<img
143+
src={require("../../../../res/img/element-icons/warning-badge.svg")}
144+
height="16"
145+
width="16"
146+
alt=""
147+
/>
148+
{_t("Data on this screen is shared with %(widgetDomain)s", {
149+
widgetDomain: parsed.hostname,
150+
})}
151+
</div>
152+
<div>
153+
<iframe
154+
ref={this.appFrame}
155+
sandbox="allow-forms allow-scripts allow-same-origin"
156+
src={widgetUrl}
157+
onLoad={this.onLoad}
158+
/>
159+
</div>
160+
<div className="mx_ModalWidgetDialog_buttons">
161+
{ buttons }
162+
</div>
163+
</BaseDialog>;
164+
}
165+
}

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,8 @@
17601760
"Verify session": "Verify session",
17611761
"Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.",
17621762
"Message edits": "Message edits",
1763+
"Modal Widget": "Modal Widget",
1764+
"Data on this screen is shared with %(widgetDomain)s": "Data on this screen is shared with %(widgetDomain)s",
17631765
"Your account is not secure": "Your account is not secure",
17641766
"Your password": "Your password",
17651767
"This session, or the other session": "This session, or the other session",

src/stores/ModalWidgetStore.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
18+
import defaultDispatcher from "../dispatcher/dispatcher";
19+
import { ActionPayload } from "../dispatcher/payloads";
20+
import Modal, {IHandle, IModal} from "../Modal";
21+
import ModalWidgetDialog from "../components/views/dialogs/ModalWidgetDialog";
22+
import {WidgetMessagingStore} from "./widgets/WidgetMessagingStore";
23+
import {IModalWidgetOpenRequestData, IModalWidgetReturnData, Widget} from "matrix-widget-api";
24+
25+
interface IState {
26+
modal?: IModal<any>;
27+
openedFromId?: string;
28+
}
29+
30+
export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
31+
private static internalInstance = new ModalWidgetStore();
32+
private modalInstance: IHandle<void[]> = null;
33+
private openSourceWidgetId: string = null;
34+
35+
private constructor() {
36+
super(defaultDispatcher, {});
37+
}
38+
39+
public static get instance(): ModalWidgetStore {
40+
return ModalWidgetStore.internalInstance;
41+
}
42+
43+
protected async onAction(payload: ActionPayload): Promise<any> {
44+
// nothing
45+
}
46+
47+
public canOpenModalWidget = () => {
48+
return !this.modalInstance;
49+
};
50+
51+
public openModalWidget = (requestData: IModalWidgetOpenRequestData, sourceWidget: Widget) => {
52+
if (this.modalInstance) return;
53+
this.openSourceWidgetId = sourceWidget.id;
54+
this.modalInstance = Modal.createTrackedDialog('Modal Widget', '', ModalWidgetDialog, {
55+
widgetDefinition: {...requestData},
56+
sourceWidgetId: sourceWidget.id,
57+
onFinished: (success: boolean, data?: IModalWidgetReturnData) => {
58+
if (!success) {
59+
this.closeModalWidget(sourceWidget, { "m.exited": true });
60+
} else {
61+
this.closeModalWidget(sourceWidget, data);
62+
}
63+
64+
this.openSourceWidgetId = null;
65+
this.modalInstance = null;
66+
},
67+
});
68+
};
69+
70+
public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => {
71+
if (!this.modalInstance) return;
72+
if (this.openSourceWidgetId === sourceWidget.id) {
73+
this.openSourceWidgetId = null;
74+
this.modalInstance.close();
75+
this.modalInstance = null;
76+
77+
const sourceMessaging = WidgetMessagingStore.instance.getMessaging(sourceWidget);
78+
if (!sourceMessaging) {
79+
console.error("No source widget messaging for modal widget");
80+
return;
81+
}
82+
sourceMessaging.notifyModalWidgetClose(data);
83+
}
84+
};
85+
}
86+
87+
window.mxModalWidgetStore = ModalWidgetStore.instance;

0 commit comments

Comments
 (0)