();
+
+ constructor(props: IProps) {
+ super(props);
+
+ const parsedEvents = WidgetEventCapability.findEventCapabilities(this.props.requestedCapabilities);
+ parsedEvents.forEach(e => this.eventPermissionsMap.set(e.raw, e));
+
+ const states: IBooleanStates = {};
+ this.props.requestedCapabilities.forEach(c => states[c] = true);
+
+ this.state = {
+ booleanStates: states,
+ rememberSelection: true,
+ };
+ }
+
+ private onToggle = (capability: Capability) => {
+ const newStates = objectShallowClone(this.state.booleanStates);
+ newStates[capability] = !newStates[capability];
+ this.setState({booleanStates: newStates});
+ };
+
+ private onRememberSelectionChange = (newVal: boolean) => {
+ this.setState({rememberSelection: newVal});
+ };
+
+ private onSubmit = async (ev) => {
+ this.closeAndTryRemember(Object.entries(this.state.booleanStates)
+ .filter(([_, isSelected]) => isSelected)
+ .map(([cap]) => cap));
+ };
+
+ private onReject = async (ev) => {
+ this.closeAndTryRemember([]); // nothing was approved
+ };
+
+ private closeAndTryRemember(approved: Capability[]) {
+ if (this.state.rememberSelection) {
+ setRememberedCapabilitiesForWidget(this.props.widget, approved);
+ }
+ this.props.onFinished({approved});
+ }
+
+ public render() {
+ const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => {
+ const evCap = this.eventPermissionsMap.get(cap);
+
+ let text: TranslatedString;
+ let byline: TranslatedString;
+ if (evCap) {
+ const t = textForEventCapabilitiy(evCap);
+ text = t.primary;
+ byline = t.byline;
+ } else if (SIMPLE_CAPABILITY_MESSAGES[cap]) {
+ text = _t(SIMPLE_CAPABILITY_MESSAGES[cap]);
+ } else {
+ text = _t(
+ "The %(capability)s
capability",
+ {capability: cap}, {code: sub => {sub}
},
+ );
+ }
+
+ return (
+
+ this.onToggle(cap)}
+ >{text}
+ {byline ? {byline} : null}
+
+ );
+ });
+
+ return (
+
+
+
+ );
+ }
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e6ba79295e..d17da7be15 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2123,9 +2123,41 @@
"Upload Error": "Upload Error",
"Verify other session": "Verify other session",
"Verification Request": "Verification Request",
+ "Remain on your screen while running": "Remain on your screen while running",
+ "Send stickers into your active room": "Send stickers into your active room",
+ "Change which room you're viewing": "Change which room you're viewing",
+ "Change the topic of your active room": "Change the topic of your active room",
+ "See when the topic changes in your active room": "See when the topic changes in your active room",
+ "Change the name of your active room": "Change the name of your active room",
+ "See when the name changes in your active room": "See when the name changes in your active room",
+ "Change the avatar of your active room": "Change the avatar of your active room",
+ "See when the avatar changes in your active room": "See when the avatar changes in your active room",
+ "with state key %(stateKey)s": "with state key %(stateKey)s",
+ "with an empty state key": "with an empty state key",
+ "See messages sent in your active room": "See messages sent in your active room",
+ "See text messages sent in your active room": "See text messages sent in your active room",
+ "See emotes sent in your active room": "See emotes sent in your active room",
+ "See images sent in your active room": "See images sent in your active room",
+ "See videos sent in your active room": "See videos sent in your active room",
+ "See general files sent in your active room": "See general files sent in your active room",
+ "See %(msgtype)s
messages sent in your active room": "See %(msgtype)s
messages sent in your active room",
+ "Send messages as you in your active room": "Send messages as you in your active room",
+ "Send text messages as you in your active room": "Send text messages as you in your active room",
+ "Send emotes as you in your active room": "Send emotes as you in your active room",
+ "Send images as you in your active room": "Send images as you in your active room",
+ "Send videos as you in your active room": "Send videos as you in your active room",
+ "Send general files as you in your active room": "Send general files as you in your active room",
+ "Send %(msgtype)s
messages as you in your active room": "Send %(msgtype)s
messages as you in your active room",
+ "See %(eventType)s
events sent in your active room": "See %(eventType)s
events sent in your active room",
+ "Send %(eventType)s
events as you in your active room": "Send %(eventType)s
events as you in your active room",
+ "The %(capability)s
capability": "The %(capability)s
capability",
+ "Approve widget permissions": "Approve widget permissions",
+ "This widget would like to:": "This widget would like to:",
+ "Remember my selection for this widget": "Remember my selection for this widget",
+ "Approve": "Approve",
+ "Decline All": "Decline All",
"A widget would like to verify your identity": "A widget would like to verify your identity",
"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.",
- "Remember my selection for this widget": "Remember my selection for this widget",
"Allow": "Allow",
"Deny": "Deny",
"Wrong file type": "Wrong file type",
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index 0921b65137..b61f57d4b3 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -103,6 +103,8 @@ export interface IVariables {
type Tags = Record React.ReactNode>;
+export type TranslatedString = string | React.ReactNode;
+
/*
* Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
* @param {string} text The untranslated text, e.g "click here now to %(foo)s".
@@ -121,7 +123,7 @@ type Tags = Record React.ReactNode>;
*/
export function _t(text: string, variables?: IVariables): string;
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
-export function _t(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode {
+export function _t(text: string, variables?: IVariables, tags?: Tags): TranslatedString {
// Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components
// However, still pass the variables to counterpart so that it can choose the correct plural if count is given
// It is enough to pass the count variable, but in the future counterpart might make use of other information too
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 73399a5086..aed07ea32b 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -32,7 +32,8 @@ import {
Widget,
WidgetApiToWidgetAction,
WidgetApiFromWidgetAction,
- IModalWidgetOpenRequest, IWidgetApiErrorResponseData,
+ IModalWidgetOpenRequest,
+ IWidgetApiErrorResponseData,
} from "matrix-widget-api";
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
import { EventEmitter } from "events";
@@ -302,7 +303,7 @@ export class StopGapWidget extends EventEmitter {
public start(iframe: HTMLIFrameElement) {
if (this.started) return;
const allowedCapabilities = this.appTileProps.whitelistCapabilities || [];
- const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget.type);
+ const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget);
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
this.messaging.on("preparing", () => this.emit("preparing"));
this.messaging.on("ready", () => this.emit("ready"));
diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index e2dbf3568e..99b6aacb26 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -14,47 +14,57 @@
* limitations under the License.
*/
-import { Capability, ISendEventDetails, WidgetDriver, WidgetEventCapability, WidgetType } from "matrix-widget-api";
-import { iterableUnion } from "../../utils/iterables";
+import {
+ Capability,
+ ISendEventDetails,
+ MatrixCapabilities, Widget,
+ WidgetDriver,
+} from "matrix-widget-api";
+import { iterableDiff, iterableUnion } from "../../utils/iterables";
import { MatrixClientPeg } from "../../MatrixClientPeg";
-import { arrayFastClone } from "../../utils/arrays";
-import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
import ActiveRoomObserver from "../../ActiveRoomObserver";
+import Modal from "../../Modal";
+import WidgetCapabilitiesPromptDialog, { getRememberedCapabilitiesForWidget } from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
// TODO: Purge this from the universe
export class StopGapWidgetDriver extends WidgetDriver {
- constructor(private allowedCapabilities: Capability[], private forType: WidgetType) {
+ private allowedCapabilities: Set;
+
+ constructor(allowedCapabilities: Capability[], private forWidget: Widget) {
super();
+
+ // Always allow screenshots to be taken because it's a client-induced flow. The widget can't
+ // spew screenshots at us and can't request screenshots of us, so it's up to us to provide the
+ // button if the widget says it supports screenshots.
+ this.allowedCapabilities = new Set([...allowedCapabilities, MatrixCapabilities.Screenshots]);
}
public async validateCapabilities(requested: Set): Promise> {
- // TODO: All of this should be a capabilities prompt.
- // See https://github.com/vector-im/element-web/issues/13111
-
- // Note: None of this well-known widget permissions stuff is documented intentionally. We
- // do not want to encourage people relying on this, but need to be able to support it at
- // the moment.
- //
- // If you're a widget developer and seeing this message, please ask the Element team if
- // it is safe for you to use this permissions system before trying to use it - it might
- // not be here in the future.
-
- const wkPerms = (MatrixClientPeg.get().getClientWellKnown() || {})['io.element.widget_permissions'];
- const allowedCaps = arrayFastClone(this.allowedCapabilities);
- if (wkPerms) {
- if (Array.isArray(wkPerms["view_room_action"])) {
- if (wkPerms["view_room_action"].includes(this.forType)) {
- allowedCaps.push(ElementWidgetCapabilities.CanChangeViewedRoom);
- }
- }
- if (Array.isArray(wkPerms["event_actions"])) {
- if (wkPerms["event_actions"].includes(this.forType)) {
- allowedCaps.push(...WidgetEventCapability.findEventCapabilities(requested).map(c => c.raw));
- }
+ // Check to see if any capabilities aren't automatically accepted (such as sticker pickers
+ // allowing stickers to be sent). If there are excess capabilities to be approved, the user
+ // will be prompted to accept them.
+ const diff = iterableDiff(requested, this.allowedCapabilities);
+ const missing = new Set(diff.removed); // "removed" is "in A (requested) but not in B (allowed)"
+ const allowedSoFar = new Set(this.allowedCapabilities);
+ getRememberedCapabilitiesForWidget(this.forWidget).forEach(cap => allowedSoFar.add(cap));
+ // TODO: Do something when the widget requests new capabilities not yet asked for
+ if (missing.size > 0) {
+ try {
+ const [result] = await Modal.createTrackedDialog(
+ 'Approve Widget Caps', '',
+ WidgetCapabilitiesPromptDialog,
+ {
+ requestedCapabilities: missing,
+ widget: this.forWidget,
+ }).finished;
+ (result.approved || []).forEach(cap => allowedSoFar.add(cap));
+ } catch (e) {
+ console.error("Non-fatal error getting capabilities: ", e);
}
}
- return new Set(iterableUnion(requested, allowedCaps));
+
+ return new Set(iterableUnion(allowedSoFar, requested));
}
public async sendEvent(eventType: string, content: any, stateKey: string = null): Promise {
diff --git a/src/utils/iterables.ts b/src/utils/iterables.ts
index 56e0bca1b7..7883b2257a 100644
--- a/src/utils/iterables.ts
+++ b/src/utils/iterables.ts
@@ -14,8 +14,12 @@
* limitations under the License.
*/
-import { arrayUnion } from "./arrays";
+import { arrayDiff, arrayUnion } from "./arrays";
export function iterableUnion(a: Iterable, b: Iterable): Iterable {
return arrayUnion(Array.from(a), Array.from(b));
}
+
+export function iterableDiff(a: Iterable, b: Iterable): { added: Iterable, removed: Iterable } {
+ return arrayDiff(Array.from(a), Array.from(b));
+}