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

Commit a212dab

Browse files
committed
Developer design a permissions dialog
1 parent 9455054 commit a212dab

File tree

8 files changed

+382
-34
lines changed

8 files changed

+382
-34
lines changed

res/css/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
@import "./views/dialogs/_TermsDialog.scss";
9292
@import "./views/dialogs/_UploadConfirmDialog.scss";
9393
@import "./views/dialogs/_UserSettingsDialog.scss";
94+
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss";
9495
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
9596
@import "./views/dialogs/security/_AccessSecretStorageDialog.scss";
9697
@import "./views/dialogs/security/_CreateCrossSigningDialog.scss";
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
18+
.mx_WidgetCapabilitiesPromptDialog {
19+
.mx_Dialog_content {
20+
margin-bottom: 16px;
21+
}
22+
23+
.mx_WidgetCapabilitiesPromptDialog_cap {
24+
margin-top: 8px;
25+
26+
.mx_WidgetCapabilitiesPromptDialog_byline {
27+
color: $muted-fg-color;
28+
margin-left: 26px;
29+
}
30+
}
31+
32+
.mx_SettingsFlag {
33+
margin-top: 24px;
34+
35+
.mx_ToggleSwitch {
36+
display: inline-block;
37+
vertical-align: middle;
38+
margin-right: 8px;
39+
}
40+
41+
.mx_SettingsFlag_label {
42+
display: inline-block;
43+
vertical-align: middle;
44+
}
45+
}
46+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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 React from 'react';
18+
import BaseDialog from "./BaseDialog";
19+
import { _t, _td, TranslatedString } from "../../../languageHandler";
20+
import { IDialogProps } from "./IDialogProps";
21+
import { Capability, EventDirection, MatrixCapabilities, Widget, WidgetEventCapability } from "matrix-widget-api";
22+
import { objectShallowClone } from "../../../utils/objects";
23+
import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities";
24+
import { EventType, MsgType } from "matrix-js-sdk/lib/@types/event";
25+
import StyledCheckbox from "../elements/StyledCheckbox";
26+
import DialogButtons from "../elements/DialogButtons";
27+
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
28+
29+
// TODO: These messaging things can probably get their own store of some sort
30+
const SIMPLE_CAPABILITY_MESSAGES = {
31+
[MatrixCapabilities.AlwaysOnScreen]: _td("Remain on your screen while running"),
32+
[MatrixCapabilities.StickerSending]: _td("Send stickers into your active room"),
33+
[ElementWidgetCapabilities.CanChangeViewedRoom]: _td("Change which room you're viewing"),
34+
};
35+
const SEND_RECV_EVENT_CAPABILITY_MESSAGES = {
36+
[EventType.RoomTopic]: {
37+
// TODO: We probably want to say "this room" when we can
38+
[EventDirection.Send]: _td("Change the topic of your active room"),
39+
[EventDirection.Receive]: _td("See when the topic changes in your active room"),
40+
},
41+
[EventType.RoomName]: {
42+
[EventDirection.Send]: _td("Change the name of your active room"),
43+
[EventDirection.Receive]: _td("See when the name changes in your active room"),
44+
},
45+
[EventType.RoomAvatar]: {
46+
[EventDirection.Send]: _td("Change the avatar of your active room"),
47+
[EventDirection.Receive]: _td("See when the avatar changes in your active room"),
48+
},
49+
// TODO: Add more as needed
50+
};
51+
function textForEventCapabilitiy(cap: WidgetEventCapability): { primary: TranslatedString, byline: TranslatedString } {
52+
let primary: TranslatedString;
53+
let byline: TranslatedString;
54+
55+
if (cap.isState) {
56+
byline = cap.keyStr
57+
? _t("with state key %(stateKey)s", {stateKey: cap.keyStr})
58+
: _t("with an empty state key");
59+
}
60+
61+
const srMessages = SEND_RECV_EVENT_CAPABILITY_MESSAGES[cap.eventType];
62+
if (srMessages && srMessages[cap.direction]) {
63+
primary = _t(srMessages[cap.direction]);
64+
} else {
65+
if (cap.eventType === EventType.RoomMessage) {
66+
if (cap.direction === EventDirection.Receive) {
67+
if (!cap.keyStr) {
68+
primary = _t("See messages sent in your active room");
69+
} else {
70+
if (cap.keyStr === MsgType.Text) {
71+
primary = _t("See text messages sent in your active room");
72+
} else if (cap.keyStr === MsgType.Emote) {
73+
primary = _t("See emotes sent in your active room");
74+
} else if (cap.keyStr === MsgType.Image) {
75+
primary = _t("See images sent in your active room");
76+
} else if (cap.keyStr === MsgType.Video) {
77+
primary = _t("See videos sent in your active room");
78+
} else if (cap.keyStr === MsgType.File) {
79+
primary = _t("See general files sent in your active room");
80+
} else {
81+
primary = _t(
82+
"See <code>%(msgtype)s</code> messages sent in your active room",
83+
{msgtype: cap.keyStr}, {code: sub => <code>{sub}</code>},
84+
);
85+
}
86+
}
87+
} else {
88+
if (!cap.keyStr) {
89+
primary = _t("Send messages as you in your active room");
90+
} else {
91+
if (cap.keyStr === MsgType.Text) {
92+
primary = _t("Send text messages as you in your active room");
93+
} else if (cap.keyStr === MsgType.Emote) {
94+
primary = _t("Send emotes as you in your active room");
95+
} else if (cap.keyStr === MsgType.Image) {
96+
primary = _t("Send images as you in your active room");
97+
} else if (cap.keyStr === MsgType.Video) {
98+
primary = _t("Send videos as you in your active room");
99+
} else if (cap.keyStr === MsgType.File) {
100+
primary = _t("Send general files as you in your active room");
101+
} else {
102+
primary = _t(
103+
"Send <code>%(msgtype)s</code> messages as you in your active room",
104+
{msgtype: cap.keyStr}, {code: sub => <code>{sub}</code>},
105+
);
106+
}
107+
}
108+
}
109+
} else {
110+
if (cap.direction === EventDirection.Receive) {
111+
primary = _t(
112+
"See <code>%(eventType)s</code> events sent in your active room",
113+
{eventType: cap.eventType}, {code: sub => <code>{sub}</code>},
114+
);
115+
} else {
116+
primary = _t(
117+
"Send <code>%(eventType)s</code> events as you in your active room",
118+
{eventType: cap.eventType}, {code: sub => <code>{sub}</code>},
119+
);
120+
}
121+
}
122+
}
123+
124+
return {primary, byline};
125+
}
126+
127+
export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
128+
return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
129+
}
130+
131+
function setRememberedCapabilitiesForWidget(widget: Widget, caps: Capability[]) {
132+
localStorage.setItem(`widget_${widget.id}_approved_caps`, JSON.stringify(caps));
133+
}
134+
135+
interface IProps extends IDialogProps {
136+
requestedCapabilities: Set<Capability>;
137+
widget: Widget;
138+
}
139+
140+
interface IBooleanStates {
141+
// @ts-ignore - TS wants a string key, but we know better
142+
[capability: Capability]: boolean;
143+
}
144+
145+
interface IState {
146+
booleanStates: IBooleanStates;
147+
rememberSelection: boolean;
148+
}
149+
150+
export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<IProps, IState> {
151+
private eventPermissionsMap = new Map<Capability, WidgetEventCapability>();
152+
153+
constructor(props: IProps) {
154+
super(props);
155+
156+
const parsedEvents = WidgetEventCapability.findEventCapabilities(this.props.requestedCapabilities);
157+
parsedEvents.forEach(e => this.eventPermissionsMap.set(e.raw, e));
158+
159+
const states: IBooleanStates = {};
160+
this.props.requestedCapabilities.forEach(c => states[c] = true);
161+
162+
this.state = {
163+
booleanStates: states,
164+
rememberSelection: true,
165+
};
166+
}
167+
168+
private onToggle = (capability: Capability) => {
169+
const newStates = objectShallowClone(this.state.booleanStates);
170+
newStates[capability] = !newStates[capability];
171+
this.setState({booleanStates: newStates});
172+
};
173+
174+
private onRememberSelectionChange = (newVal: boolean) => {
175+
this.setState({rememberSelection: newVal});
176+
};
177+
178+
private onSubmit = async (ev) => {
179+
this.closeAndTryRemember(Object.entries(this.state.booleanStates)
180+
.filter(([_, isSelected]) => isSelected)
181+
.map(([cap]) => cap));
182+
};
183+
184+
private onReject = async (ev) => {
185+
this.closeAndTryRemember([]); // nothing was approved
186+
};
187+
188+
private closeAndTryRemember(approved: Capability[]) {
189+
if (this.state.rememberSelection) {
190+
setRememberedCapabilitiesForWidget(this.props.widget, approved);
191+
}
192+
this.props.onFinished({approved});
193+
}
194+
195+
public render() {
196+
const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => {
197+
const evCap = this.eventPermissionsMap.get(cap);
198+
199+
let text: TranslatedString;
200+
let byline: TranslatedString;
201+
if (evCap) {
202+
const t = textForEventCapabilitiy(evCap);
203+
text = t.primary;
204+
byline = t.byline;
205+
} else if (SIMPLE_CAPABILITY_MESSAGES[cap]) {
206+
text = _t(SIMPLE_CAPABILITY_MESSAGES[cap]);
207+
} else {
208+
text = _t(
209+
"The <code>%(capability)s</code> capability",
210+
{capability: cap}, {code: sub => <code>{sub}</code>},
211+
);
212+
}
213+
214+
return (
215+
<div className="mx_WidgetCapabilitiesPromptDialog_cap">
216+
<StyledCheckbox
217+
key={cap + i}
218+
checked={isChecked}
219+
onChange={() => this.onToggle(cap)}
220+
>{text}</StyledCheckbox>
221+
{byline ? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{byline}</span> : null}
222+
</div>
223+
);
224+
});
225+
226+
return (
227+
<BaseDialog
228+
className="mx_WidgetCapabilitiesPromptDialog"
229+
onFinished={this.props.onFinished}
230+
title={_t("Approve widget permissions")}
231+
>
232+
<form onSubmit={this.onSubmit}>
233+
<div className="mx_Dialog_content">
234+
{_t("This widget would like to:")}
235+
{checkboxRows}
236+
<LabelledToggleSwitch
237+
value={this.state.rememberSelection}
238+
toggleInFront={true}
239+
onChange={this.onRememberSelectionChange}
240+
label={_t("Remember my selection for this widget")} />
241+
<DialogButtons
242+
primaryButton={_t("Approve")}
243+
cancelButton={_t("Decline All")}
244+
onPrimaryButtonClick={this.onSubmit}
245+
onCancel={this.onReject}
246+
/>
247+
</div>
248+
</form>
249+
</BaseDialog>
250+
);
251+
}
252+
}

src/i18n/strings/en_EN.json

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2123,9 +2123,41 @@
21232123
"Upload Error": "Upload Error",
21242124
"Verify other session": "Verify other session",
21252125
"Verification Request": "Verification Request",
2126+
"Remain on your screen while running": "Remain on your screen while running",
2127+
"Send stickers into your active room": "Send stickers into your active room",
2128+
"Change which room you're viewing": "Change which room you're viewing",
2129+
"Change the topic of your active room": "Change the topic of your active room",
2130+
"See when the topic changes in your active room": "See when the topic changes in your active room",
2131+
"Change the name of your active room": "Change the name of your active room",
2132+
"See when the name changes in your active room": "See when the name changes in your active room",
2133+
"Change the avatar of your active room": "Change the avatar of your active room",
2134+
"See when the avatar changes in your active room": "See when the avatar changes in your active room",
2135+
"with state key %(stateKey)s": "with state key %(stateKey)s",
2136+
"with an empty state key": "with an empty state key",
2137+
"See messages sent in your active room": "See messages sent in your active room",
2138+
"See text messages sent in your active room": "See text messages sent in your active room",
2139+
"See emotes sent in your active room": "See emotes sent in your active room",
2140+
"See images sent in your active room": "See images sent in your active room",
2141+
"See videos sent in your active room": "See videos sent in your active room",
2142+
"See general files sent in your active room": "See general files sent in your active room",
2143+
"See <code>%(msgtype)s</code> messages sent in your active room": "See <code>%(msgtype)s</code> messages sent in your active room",
2144+
"Send messages as you in your active room": "Send messages as you in your active room",
2145+
"Send text messages as you in your active room": "Send text messages as you in your active room",
2146+
"Send emotes as you in your active room": "Send emotes as you in your active room",
2147+
"Send images as you in your active room": "Send images as you in your active room",
2148+
"Send videos as you in your active room": "Send videos as you in your active room",
2149+
"Send general files as you in your active room": "Send general files as you in your active room",
2150+
"Send <code>%(msgtype)s</code> messages as you in your active room": "Send <code>%(msgtype)s</code> messages as you in your active room",
2151+
"See <code>%(eventType)s</code> events sent in your active room": "See <code>%(eventType)s</code> events sent in your active room",
2152+
"Send <code>%(eventType)s</code> events as you in your active room": "Send <code>%(eventType)s</code> events as you in your active room",
2153+
"The <code>%(capability)s</code> capability": "The <code>%(capability)s</code> capability",
2154+
"Approve widget permissions": "Approve widget permissions",
2155+
"This widget would like to:": "This widget would like to:",
2156+
"Remember my selection for this widget": "Remember my selection for this widget",
2157+
"Approve": "Approve",
2158+
"Decline All": "Decline All",
21262159
"A widget would like to verify your identity": "A widget would like to verify your identity",
21272160
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.",
2128-
"Remember my selection for this widget": "Remember my selection for this widget",
21292161
"Allow": "Allow",
21302162
"Deny": "Deny",
21312163
"Wrong file type": "Wrong file type",

src/languageHandler.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ export interface IVariables {
103103

104104
type Tags = Record<string, (sub: string) => React.ReactNode>;
105105

106+
export type TranslatedString = string | React.ReactNode;
107+
106108
/*
107109
* Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
108110
* @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s".
@@ -121,7 +123,7 @@ type Tags = Record<string, (sub: string) => React.ReactNode>;
121123
*/
122124
export function _t(text: string, variables?: IVariables): string;
123125
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
124-
export function _t(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode {
126+
export function _t(text: string, variables?: IVariables, tags?: Tags): TranslatedString {
125127
// Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components
126128
// However, still pass the variables to counterpart so that it can choose the correct plural if count is given
127129
// It is enough to pass the count variable, but in the future counterpart might make use of other information too

src/stores/widgets/StopGapWidget.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {
3232
Widget,
3333
WidgetApiToWidgetAction,
3434
WidgetApiFromWidgetAction,
35-
IModalWidgetOpenRequest, IWidgetApiErrorResponseData,
35+
IModalWidgetOpenRequest,
36+
IWidgetApiErrorResponseData,
3637
} from "matrix-widget-api";
3738
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
3839
import { EventEmitter } from "events";
@@ -302,7 +303,7 @@ export class StopGapWidget extends EventEmitter {
302303
public start(iframe: HTMLIFrameElement) {
303304
if (this.started) return;
304305
const allowedCapabilities = this.appTileProps.whitelistCapabilities || [];
305-
const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget.type);
306+
const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget);
306307
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
307308
this.messaging.on("preparing", () => this.emit("preparing"));
308309
this.messaging.on("ready", () => this.emit("ready"));

0 commit comments

Comments
 (0)