mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 03:36:07 +03:00
Merge pull request #5385 from matrix-org/travis/msc-send-widget-events
Add new widget API actions for changing rooms and sending/receiving events
This commit is contained in:
commit
5f47077a30
16 changed files with 835 additions and 15 deletions
|
@ -79,7 +79,7 @@
|
|||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.19",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^0.1.0-beta.8",
|
||||
"matrix-widget-api": "^0.1.0-beta.9",
|
||||
"minimist": "^1.2.5",
|
||||
"pako": "^1.0.11",
|
||||
"parse5": "^5.1.1",
|
||||
|
|
|
@ -60,6 +60,10 @@ pre, code {
|
|||
color: $accent-color;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
|
||||
b {
|
||||
// On Firefox, the default weight for `<b>` is `bolder` which results in no bold
|
||||
// effect since we only have specific weights of our fonts available.
|
||||
|
@ -364,6 +368,11 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
.mx_Dialog_buttons {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
|
||||
.mx_Dialog_buttons_additive {
|
||||
// The consumer is responsible for positioning their elements.
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
/* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied
|
||||
|
|
|
@ -91,6 +91,7 @@
|
|||
@import "./views/dialogs/_TermsDialog.scss";
|
||||
@import "./views/dialogs/_UploadConfirmDialog.scss";
|
||||
@import "./views/dialogs/_UserSettingsDialog.scss";
|
||||
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss";
|
||||
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
|
||||
@import "./views/dialogs/security/_AccessSecretStorageDialog.scss";
|
||||
@import "./views/dialogs/security/_CreateCrossSigningDialog.scss";
|
||||
|
|
75
res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.scss
Normal file
75
res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.scss
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
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_WidgetCapabilitiesPromptDialog {
|
||||
.text-muted {
|
||||
font-size: $font-12px;
|
||||
}
|
||||
|
||||
.mx_Dialog_content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mx_WidgetCapabilitiesPromptDialog_cap {
|
||||
margin-top: 20px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-15px;
|
||||
|
||||
.mx_WidgetCapabilitiesPromptDialog_byline {
|
||||
color: $muted-fg-color;
|
||||
margin-left: 26px;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Dialog_buttons {
|
||||
margin-top: 40px; // double normal
|
||||
}
|
||||
|
||||
.mx_SettingsFlag {
|
||||
line-height: calc($font-14px + 7px + 7px); // 7px top & bottom padding
|
||||
color: $muted-fg-color;
|
||||
font-size: $font-12px;
|
||||
|
||||
.mx_ToggleSwitch {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
|
||||
// downsize the switch + ball
|
||||
width: $font-32px;
|
||||
height: $font-15px;
|
||||
|
||||
|
||||
&.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball {
|
||||
left: calc(100% - $font-15px);
|
||||
}
|
||||
|
||||
.mx_ToggleSwitch_ball {
|
||||
width: $font-15px;
|
||||
height: $font-15px;
|
||||
border-radius: $font-15px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SettingsFlag_label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import {
|
|||
ModalButtonKind,
|
||||
Widget,
|
||||
WidgetApiFromWidgetAction,
|
||||
WidgetKind,
|
||||
} from "matrix-widget-api";
|
||||
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
@ -72,7 +73,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const driver = new StopGapWidgetDriver( []);
|
||||
const driver = new StopGapWidgetDriver( [], this.widget, WidgetKind.Modal);
|
||||
const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
|
||||
this.setState({messaging});
|
||||
}
|
||||
|
|
147
src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
Normal file
147
src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
Normal file
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import {
|
||||
Capability,
|
||||
Widget,
|
||||
WidgetEventCapability,
|
||||
WidgetKind,
|
||||
} from "matrix-widget-api";
|
||||
import { objectShallowClone } from "../../../utils/objects";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import { CapabilityText } from "../../../widgets/CapabilityText";
|
||||
|
||||
export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
|
||||
return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
|
||||
}
|
||||
|
||||
function setRememberedCapabilitiesForWidget(widget: Widget, caps: Capability[]) {
|
||||
localStorage.setItem(`widget_${widget.id}_approved_caps`, JSON.stringify(caps));
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
requestedCapabilities: Set<Capability>;
|
||||
widget: Widget;
|
||||
widgetKind: WidgetKind; // TODO: Refactor into the Widget class
|
||||
}
|
||||
|
||||
interface IBooleanStates {
|
||||
// @ts-ignore - TS wants a string key, but we know better
|
||||
[capability: Capability]: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
booleanStates: IBooleanStates;
|
||||
rememberSelection: boolean;
|
||||
}
|
||||
|
||||
export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<IProps, IState> {
|
||||
private eventPermissionsMap = new Map<Capability, WidgetEventCapability>();
|
||||
|
||||
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 text = CapabilityText.for(cap, this.props.widgetKind);
|
||||
const byline = text.byline
|
||||
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{text.byline}</span>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx_WidgetCapabilitiesPromptDialog_cap" key={cap + i}>
|
||||
<StyledCheckbox
|
||||
checked={isChecked}
|
||||
onChange={() => this.onToggle(cap)}
|
||||
>{text.primary}</StyledCheckbox>
|
||||
{byline}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_WidgetCapabilitiesPromptDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Approve widget permissions")}
|
||||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="text-muted">{_t("This widget would like to:")}</div>
|
||||
{checkboxRows}
|
||||
<DialogButtons
|
||||
primaryButton={_t("Approve")}
|
||||
cancelButton={_t("Decline All")}
|
||||
onPrimaryButtonClick={this.onSubmit}
|
||||
onCancel={this.onReject}
|
||||
additive={
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.rememberSelection}
|
||||
toggleInFront={true}
|
||||
onChange={this.onRememberSelectionChange}
|
||||
label={_t("Remember my selection for this widget")} />}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -54,6 +54,9 @@ export default class DialogButtons extends React.Component {
|
|||
|
||||
// disables only the primary button
|
||||
primaryDisabled: PropTypes.bool,
|
||||
|
||||
// something to stick next to the buttons, optionally
|
||||
additive: PropTypes.element,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -85,8 +88,14 @@ export default class DialogButtons extends React.Component {
|
|||
</button>;
|
||||
}
|
||||
|
||||
let additive = null;
|
||||
if (this.props.additive) {
|
||||
additive = <div className="mx_Dialog_buttons_additive">{this.props.additive}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_Dialog_buttons">
|
||||
{ additive }
|
||||
{ cancelButton }
|
||||
{ this.props.children }
|
||||
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'}
|
||||
|
|
|
@ -569,6 +569,62 @@
|
|||
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
|
||||
"%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
|
||||
"%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …",
|
||||
"Remain on your screen when viewing another room, when running": "Remain on your screen when viewing another room, when running",
|
||||
"Remain on your screen while running": "Remain on your screen while running",
|
||||
"Send stickers into this room": "Send stickers into this room",
|
||||
"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 this room": "Change the topic of this room",
|
||||
"See when the topic changes in this room": "See when the topic changes in this room",
|
||||
"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 this room": "Change the name of this room",
|
||||
"See when the name changes in this room": "See when the name changes in this 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 this room": "Change the avatar of this room",
|
||||
"See when the avatar changes in this room": "See when the avatar changes in this 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",
|
||||
"Send stickers to this room as you": "Send stickers to this room as you",
|
||||
"See when a sticker is posted in this room": "See when a sticker is posted in this room",
|
||||
"Send stickers to your active room as you": "Send stickers to your active room as you",
|
||||
"See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room",
|
||||
"with an empty state key": "with an empty state key",
|
||||
"with state key %(stateKey)s": "with state key %(stateKey)s",
|
||||
"Send <b>%(eventType)s</b> events as you in this room": "Send <b>%(eventType)s</b> events as you in this room",
|
||||
"See <b>%(eventType)s</b> events posted to this room": "See <b>%(eventType)s</b> events posted to this room",
|
||||
"Send <b>%(eventType)s</b> events as you in your active room": "Send <b>%(eventType)s</b> events as you in your active room",
|
||||
"See <b>%(eventType)s</b> events posted to your active room": "See <b>%(eventType)s</b> events posted to your active room",
|
||||
"The <b>%(capability)s</b> capability": "The <b>%(capability)s</b> capability",
|
||||
"Send messages as you in this room": "Send messages as you in this room",
|
||||
"Send messages as you in your active room": "Send messages as you in your active room",
|
||||
"See messages posted to this room": "See messages posted to this room",
|
||||
"See messages posted to your active room": "See messages posted to your active room",
|
||||
"Send text messages as you in this room": "Send text messages as you in this room",
|
||||
"Send text messages as you in your active room": "Send text messages as you in your active room",
|
||||
"See text messages posted to this room": "See text messages posted to this room",
|
||||
"See text messages posted to your active room": "See text messages posted to your active room",
|
||||
"Send emotes as you in this room": "Send emotes as you in this room",
|
||||
"Send emotes as you in your active room": "Send emotes as you in your active room",
|
||||
"See emotes posted to this room": "See emotes posted to this room",
|
||||
"See emotes posted to your active room": "See emotes posted to your active room",
|
||||
"Send images as you in this room": "Send images as you in this room",
|
||||
"Send images as you in your active room": "Send images as you in your active room",
|
||||
"See images posted to this room": "See images posted to this room",
|
||||
"See images posted to your active room": "See images posted to your active room",
|
||||
"Send videos as you in this room": "Send videos as you in this room",
|
||||
"Send videos as you in your active room": "Send videos as you in your active room",
|
||||
"See videos posted to this room": "See videos posted to this room",
|
||||
"See videos posted to your active room": "See videos posted to your active room",
|
||||
"Send general files as you in this room": "Send general files as you in this room",
|
||||
"Send general files as you in your active room": "Send general files as you in your active room",
|
||||
"See general files posted to this room": "See general files posted to this room",
|
||||
"See general files posted to your active room": "See general files posted to your active room",
|
||||
"Send <b>%(msgtype)s</b> messages as you in this room": "Send <b>%(msgtype)s</b> messages as you in this room",
|
||||
"Send <b>%(msgtype)s</b> messages as you in your active room": "Send <b>%(msgtype)s</b> messages as you in your active room",
|
||||
"See <b>%(msgtype)s</b> messages posted to this room": "See <b>%(msgtype)s</b> messages posted to this room",
|
||||
"See <b>%(msgtype)s</b> messages posted to your active room": "See <b>%(msgtype)s</b> messages posted to your active room",
|
||||
"Cannot reach homeserver": "Cannot reach homeserver",
|
||||
"Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin",
|
||||
"Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured",
|
||||
|
@ -2125,9 +2181,13 @@
|
|||
"Upload Error": "Upload Error",
|
||||
"Verify other session": "Verify other session",
|
||||
"Verification Request": "Verification Request",
|
||||
"Approve widget permissions": "Approve widget permissions",
|
||||
"This widget would like to:": "This widget would like to:",
|
||||
"Approve": "Approve",
|
||||
"Decline All": "Decline All",
|
||||
"Remember my selection for this widget": "Remember my selection for this widget",
|
||||
"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",
|
||||
|
|
|
@ -103,6 +103,8 @@ export interface IVariables {
|
|||
|
||||
type Tags = Record<string, (sub: string) => 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 <a>here</a> now to %(foo)s".
|
||||
|
@ -121,7 +123,7 @@ type Tags = Record<string, (sub: string) => 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
|
||||
|
|
|
@ -14,8 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
export enum ElementWidgetActions {
|
||||
ClientReady = "im.vector.ready",
|
||||
HangupCall = "im.vector.hangup",
|
||||
OpenIntegrationManager = "integration_manager_open",
|
||||
ViewRoom = "io.element.view_room",
|
||||
}
|
||||
|
||||
export interface IViewRoomApiRequest extends IWidgetApiRequest {
|
||||
data: {
|
||||
room_id: string; // eslint-disable-line camelcase
|
||||
};
|
||||
}
|
||||
|
|
19
src/stores/widgets/ElementWidgetCapabilities.ts
Normal file
19
src/stores/widgets/ElementWidgetCapabilities.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum ElementWidgetCapabilities {
|
||||
CanChangeViewedRoom = "io.element.view_room",
|
||||
}
|
|
@ -33,6 +33,8 @@ import {
|
|||
WidgetApiToWidgetAction,
|
||||
WidgetApiFromWidgetAction,
|
||||
IModalWidgetOpenRequest,
|
||||
IWidgetApiErrorResponseData,
|
||||
WidgetKind,
|
||||
} from "matrix-widget-api";
|
||||
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
|
||||
import { EventEmitter } from "events";
|
||||
|
@ -47,13 +49,15 @@ import { WidgetType } from "../../widgets/WidgetType";
|
|||
import ActiveWidgetStore from "../ActiveWidgetStore";
|
||||
import { objectShallowClone } from "../../utils/objects";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ElementWidgetActions } from "./ElementWidgetActions";
|
||||
import { ElementWidgetActions, IViewRoomApiRequest } from "./ElementWidgetActions";
|
||||
import Modal from "../../Modal";
|
||||
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||
import {ModalWidgetStore} from "../ModalWidgetStore";
|
||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
// TODO: Destroy all of this code
|
||||
|
||||
|
@ -148,6 +152,8 @@ export class StopGapWidget extends EventEmitter {
|
|||
private messaging: ClientWidgetApi;
|
||||
private mockWidget: ElementWidget;
|
||||
private scalarToken: string;
|
||||
private roomId?: string;
|
||||
private kind: WidgetKind;
|
||||
|
||||
constructor(private appTileProps: IAppTileProps) {
|
||||
super();
|
||||
|
@ -160,6 +166,19 @@ export class StopGapWidget extends EventEmitter {
|
|||
}
|
||||
|
||||
this.mockWidget = new ElementWidget(app);
|
||||
this.roomId = appTileProps.room?.roomId;
|
||||
this.kind = appTileProps.userWidget ? WidgetKind.Account : WidgetKind.Room; // probably
|
||||
}
|
||||
|
||||
private get eventListenerRoomId(): string {
|
||||
// When widgets are listening to events, we need to make sure they're only
|
||||
// receiving events for the right room. In particular, room widgets get locked
|
||||
// to the room they were added in while account widgets listen to the currently
|
||||
// active room.
|
||||
|
||||
if (this.roomId) return this.roomId;
|
||||
|
||||
return RoomViewStore.getRoomId();
|
||||
}
|
||||
|
||||
public get widgetApi(): ClientWidgetApi {
|
||||
|
@ -286,7 +305,8 @@ export class StopGapWidget extends EventEmitter {
|
|||
|
||||
public start(iframe: HTMLIFrameElement) {
|
||||
if (this.started) return;
|
||||
const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
|
||||
const allowedCapabilities = this.appTileProps.whitelistCapabilities || [];
|
||||
const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget, this.kind);
|
||||
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
|
||||
this.messaging.on("preparing", () => this.emit("preparing"));
|
||||
this.messaging.on("ready", () => this.emit("ready"));
|
||||
|
@ -298,6 +318,39 @@ export class StopGapWidget extends EventEmitter {
|
|||
ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
|
||||
}
|
||||
|
||||
// Always attach a handler for ViewRoom, but permission check it internally
|
||||
this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
|
||||
ev.preventDefault(); // stop the widget API from auto-rejecting this
|
||||
|
||||
// Check up front if this is even a valid request
|
||||
const targetRoomId = (ev.detail.data || {}).room_id;
|
||||
if (!targetRoomId) {
|
||||
return this.messaging.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
|
||||
error: {message: "Room ID not supplied."},
|
||||
});
|
||||
}
|
||||
|
||||
// Check the widget's permission
|
||||
if (!this.messaging.hasCapability(ElementWidgetCapabilities.CanChangeViewedRoom)) {
|
||||
return this.messaging.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
|
||||
error: {message: "This widget does not have permission for this action (denied)."},
|
||||
});
|
||||
}
|
||||
|
||||
// at this point we can change rooms, so do that
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: targetRoomId,
|
||||
});
|
||||
|
||||
// acknowledge so the widget doesn't freak out
|
||||
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
|
||||
});
|
||||
|
||||
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
|
||||
MatrixClientPeg.get().on('event', this.onEvent);
|
||||
MatrixClientPeg.get().on('Event.decrypted', this.onEventDecrypted);
|
||||
|
||||
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
|
||||
this.messaging.on("action:set_always_on_screen",
|
||||
(ev: CustomEvent<IStickyActionRequest>) => {
|
||||
|
@ -391,5 +444,31 @@ export class StopGapWidget extends EventEmitter {
|
|||
if (!this.started) return;
|
||||
WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
|
||||
ActiveWidgetStore.delRoomId(this.mockWidget.id);
|
||||
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().off('event', this.onEvent);
|
||||
MatrixClientPeg.get().off('Event.decrypted', this.onEventDecrypted);
|
||||
}
|
||||
}
|
||||
|
||||
private onEvent = (ev: MatrixEvent) => {
|
||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
||||
if (ev.getRoomId() !== this.eventListenerRoomId) return;
|
||||
this.feedEvent(ev);
|
||||
};
|
||||
|
||||
private onEventDecrypted = (ev: MatrixEvent) => {
|
||||
if (ev.isDecryptionFailure()) return;
|
||||
if (ev.getRoomId() !== this.eventListenerRoomId) return;
|
||||
this.feedEvent(ev);
|
||||
};
|
||||
|
||||
private feedEvent(ev: MatrixEvent) {
|
||||
if (!this.messaging) return;
|
||||
|
||||
const raw = ev.event;
|
||||
this.messaging.feedEvent(raw).catch(e => {
|
||||
console.error("Error sending event to widget: ", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,17 +14,80 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Capability, WidgetDriver } from "matrix-widget-api";
|
||||
import { iterableUnion } from "../../utils/iterables";
|
||||
import {
|
||||
Capability,
|
||||
ISendEventDetails,
|
||||
MatrixCapabilities,
|
||||
Widget,
|
||||
WidgetDriver,
|
||||
WidgetKind,
|
||||
} from "matrix-widget-api";
|
||||
import { iterableDiff, iterableUnion } from "../../utils/iterables";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
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 allowedCapabilities: Set<Capability>;
|
||||
|
||||
// TODO: Refactor widgetKind into the Widget class
|
||||
constructor(allowedCapabilities: Capability[], private forWidget: Widget, private forWidgetKind: WidgetKind) {
|
||||
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<Capability>): Promise<Set<Capability>> {
|
||||
return new Set(iterableUnion(requested, this.allowedCapabilities));
|
||||
// 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,
|
||||
widgetKind: this.forWidgetKind,
|
||||
}).finished;
|
||||
(result.approved || []).forEach(cap => allowedSoFar.add(cap));
|
||||
} catch (e) {
|
||||
console.error("Non-fatal error getting capabilities: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
return new Set(iterableUnion(allowedSoFar, requested));
|
||||
}
|
||||
|
||||
public async sendEvent(eventType: string, content: any, stateKey: string = null): Promise<ISendEventDetails> {
|
||||
const client = MatrixClientPeg.get();
|
||||
const roomId = ActiveRoomObserver.activeRoomId;
|
||||
|
||||
if (!client || !roomId) throw new Error("Not in a room or not attached to a client");
|
||||
|
||||
let r: {event_id: string} = null; // eslint-disable-line camelcase
|
||||
if (stateKey !== null) {
|
||||
// state event
|
||||
r = await client.sendStateEvent(roomId, eventType, content, stateKey);
|
||||
} else {
|
||||
// message event
|
||||
r = await client.sendEvent(roomId, eventType, content);
|
||||
}
|
||||
|
||||
return {roomId, eventId: r.event_id};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { arrayUnion } from "./arrays";
|
||||
import { arrayDiff, arrayUnion } from "./arrays";
|
||||
|
||||
export function iterableUnion<T>(a: Iterable<T>, b: Iterable<T>): Iterable<T> {
|
||||
return arrayUnion(Array.from(a), Array.from(b));
|
||||
}
|
||||
|
||||
export function iterableDiff<T>(a: Iterable<T>, b: Iterable<T>): { added: Iterable<T>, removed: Iterable<T> } {
|
||||
return arrayDiff(Array.from(a), Array.from(b));
|
||||
}
|
||||
|
|
342
src/widgets/CapabilityText.tsx
Normal file
342
src/widgets/CapabilityText.tsx
Normal file
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
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 { Capability, EventDirection, MatrixCapabilities, WidgetEventCapability, WidgetKind } from "matrix-widget-api";
|
||||
import { _t, _td, TranslatedString } from "../languageHandler";
|
||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities";
|
||||
import React from "react";
|
||||
|
||||
type GENERIC_WIDGET_KIND = "generic";
|
||||
const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic";
|
||||
|
||||
interface ISendRecvStaticCapText {
|
||||
// @ts-ignore - TS wants the key to be a string, but we know better
|
||||
[eventType: EventType]: {
|
||||
// @ts-ignore - TS wants the key to be a string, but we know better
|
||||
[widgetKind: WidgetKind | GENERIC_WIDGET_KIND]: {
|
||||
// @ts-ignore - TS wants the key to be a string, but we know better
|
||||
[direction: EventDirection]: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface IStaticCapText {
|
||||
// @ts-ignore - TS wants the key to be a string, but we know better
|
||||
[capability: Capability]: {
|
||||
// @ts-ignore - TS wants the key to be a string, but we know better
|
||||
[widgetKind: WidgetKind | GENERIC_WIDGET_KIND]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TranslatedCapabilityText {
|
||||
primary: TranslatedString;
|
||||
byline?: TranslatedString;
|
||||
}
|
||||
|
||||
export class CapabilityText {
|
||||
private static simpleCaps: IStaticCapText = {
|
||||
[MatrixCapabilities.AlwaysOnScreen]: {
|
||||
[WidgetKind.Room]: _td("Remain on your screen when viewing another room, when running"),
|
||||
[GENERIC_WIDGET_KIND]: _td("Remain on your screen while running"),
|
||||
},
|
||||
[MatrixCapabilities.StickerSending]: {
|
||||
[WidgetKind.Room]: _td("Send stickers into this room"),
|
||||
[GENERIC_WIDGET_KIND]: _td("Send stickers into your active room"),
|
||||
},
|
||||
[ElementWidgetCapabilities.CanChangeViewedRoom]: {
|
||||
[GENERIC_WIDGET_KIND]: _td("Change which room you're viewing"),
|
||||
},
|
||||
};
|
||||
|
||||
private static stateSendRecvCaps: ISendRecvStaticCapText = {
|
||||
[EventType.RoomTopic]: {
|
||||
[WidgetKind.Room]: {
|
||||
[EventDirection.Send]: _td("Change the topic of this room"),
|
||||
[EventDirection.Receive]: _td("See when the topic changes in this room"),
|
||||
},
|
||||
[GENERIC_WIDGET_KIND]: {
|
||||
[EventDirection.Send]: _td("Change the topic of your active room"),
|
||||
[EventDirection.Receive]: _td("See when the topic changes in your active room"),
|
||||
},
|
||||
},
|
||||
[EventType.RoomName]: {
|
||||
[WidgetKind.Room]: {
|
||||
[EventDirection.Send]: _td("Change the name of this room"),
|
||||
[EventDirection.Receive]: _td("See when the name changes in this room"),
|
||||
},
|
||||
[GENERIC_WIDGET_KIND]: {
|
||||
[EventDirection.Send]: _td("Change the name of your active room"),
|
||||
[EventDirection.Receive]: _td("See when the name changes in your active room"),
|
||||
},
|
||||
},
|
||||
[EventType.RoomAvatar]: {
|
||||
[WidgetKind.Room]: {
|
||||
[EventDirection.Send]: _td("Change the avatar of this room"),
|
||||
[EventDirection.Receive]: _td("See when the avatar changes in this room"),
|
||||
},
|
||||
[GENERIC_WIDGET_KIND]: {
|
||||
[EventDirection.Send]: _td("Change the avatar of your active room"),
|
||||
[EventDirection.Receive]: _td("See when the avatar changes in your active room"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
private static nonStateSendRecvCaps: ISendRecvStaticCapText = {
|
||||
[EventType.Sticker]: {
|
||||
[WidgetKind.Room]: {
|
||||
[EventDirection.Send]: _td("Send stickers to this room as you"),
|
||||
[EventDirection.Receive]: _td("See when a sticker is posted in this room"),
|
||||
},
|
||||
[GENERIC_WIDGET_KIND]: {
|
||||
[EventDirection.Send]: _td("Send stickers to your active room as you"),
|
||||
[EventDirection.Receive]: _td("See when anyone posts a sticker to your active room"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
private static bylineFor(eventCap: WidgetEventCapability): TranslatedString {
|
||||
if (eventCap.isState) {
|
||||
return !eventCap.keyStr
|
||||
? _t("with an empty state key")
|
||||
: _t("with state key %(stateKey)s", {stateKey: eventCap.keyStr});
|
||||
}
|
||||
return null; // room messages are handled specially
|
||||
}
|
||||
|
||||
public static for(capability: Capability, kind: WidgetKind): TranslatedCapabilityText {
|
||||
// First see if we have a super simple line of text to provide back
|
||||
if (CapabilityText.simpleCaps[capability]) {
|
||||
const textForKind = CapabilityText.simpleCaps[capability];
|
||||
if (textForKind[kind]) return {primary: _t(textForKind[kind])};
|
||||
if (textForKind[GENERIC_WIDGET_KIND]) return {primary: _t(textForKind[GENERIC_WIDGET_KIND])};
|
||||
|
||||
// ... we'll fall through to the generic capability processing at the end of this
|
||||
// function if we fail to locate a simple string and the capability isn't for an
|
||||
// event.
|
||||
}
|
||||
|
||||
// We didn't have a super simple line of text, so try processing the capability as the
|
||||
// more complex event send/receive permission type.
|
||||
const [eventCap] = WidgetEventCapability.findEventCapabilities([capability]);
|
||||
if (eventCap) {
|
||||
// Special case room messages so they show up a bit cleaner to the user. Result is
|
||||
// effectively "Send images" instead of "Send messages... of type images" if we were
|
||||
// to handle the msgtype nuances in this function.
|
||||
if (!eventCap.isState && eventCap.eventType === EventType.RoomMessage) {
|
||||
return CapabilityText.forRoomMessageCap(eventCap, kind);
|
||||
}
|
||||
|
||||
// See if we have a static line of text to provide for the given event type and
|
||||
// direction. The hope is that we do for common event types for friendlier copy.
|
||||
const evSendRecv = eventCap.isState
|
||||
? CapabilityText.stateSendRecvCaps
|
||||
: CapabilityText.nonStateSendRecvCaps;
|
||||
if (evSendRecv[eventCap.eventType]) {
|
||||
const textForKind = evSendRecv[eventCap.eventType];
|
||||
const textForDirection = textForKind[kind] || textForKind[GENERIC_WIDGET_KIND];
|
||||
if (textForDirection && textForDirection[eventCap.direction]) {
|
||||
return {
|
||||
primary: _t(textForDirection[eventCap.direction]),
|
||||
// no byline because we would have already represented the event properly
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have anything simple, so just return a generic string for the event cap
|
||||
if (kind === WidgetKind.Room) {
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
return {
|
||||
primary: _t("Send <b>%(eventType)s</b> events as you in this room", {
|
||||
eventType: eventCap.eventType,
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
}),
|
||||
byline: CapabilityText.bylineFor(eventCap),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: _t("See <b>%(eventType)s</b> events posted to this room", {
|
||||
eventType: eventCap.eventType,
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
}),
|
||||
byline: CapabilityText.bylineFor(eventCap),
|
||||
};
|
||||
}
|
||||
} else { // assume generic
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
return {
|
||||
primary: _t("Send <b>%(eventType)s</b> events as you in your active room", {
|
||||
eventType: eventCap.eventType,
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
}),
|
||||
byline: CapabilityText.bylineFor(eventCap),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: _t("See <b>%(eventType)s</b> events posted to your active room", {
|
||||
eventType: eventCap.eventType,
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
}),
|
||||
byline: CapabilityText.bylineFor(eventCap),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have enough context to render this capability specially, so we'll present it as-is
|
||||
return {
|
||||
primary: _t("The <b>%(capability)s</b> capability", {capability}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private static forRoomMessageCap(eventCap: WidgetEventCapability, kind: WidgetKind): TranslatedCapabilityText {
|
||||
// First handle the case of "all messages" to make the switch later on a bit clearer
|
||||
if (!eventCap.keyStr) {
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("Send messages as you in this room")
|
||||
: _t("Send messages as you in your active room"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("See messages posted to this room")
|
||||
: _t("See messages posted to your active room"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Now handle all the message types we care about. There are more message types available, however
|
||||
// they are not as common so we don't bother rendering them. They'll fall into the generic case.
|
||||
switch (eventCap.keyStr) {
|
||||
case MsgType.Text: {
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("Send text messages as you in this room")
|
||||
: _t("Send text messages as you in your active room"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("See text messages posted to this room")
|
||||
: _t("See text messages posted to your active room"),
|
||||
};
|
||||
}
|
||||
}
|
||||
case MsgType.Emote: {
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("Send emotes as you in this room")
|
||||
: _t("Send emotes as you in your active room"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("See emotes posted to this room")
|
||||
: _t("See emotes posted to your active room"),
|
||||
};
|
||||
}
|
||||
}
|
||||
case MsgType.Image: {
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("Send images as you in this room")
|
||||
: _t("Send images as you in your active room"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("See images posted to this room")
|
||||
: _t("See images posted to your active room"),
|
||||
};
|
||||
}
|
||||
}
|
||||
case MsgType.Video: {
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("Send videos as you in this room")
|
||||
: _t("Send videos as you in your active room"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("See videos posted to this room")
|
||||
: _t("See videos posted to your active room"),
|
||||
};
|
||||
}
|
||||
}
|
||||
case MsgType.File: {
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("Send general files as you in this room")
|
||||
: _t("Send general files as you in your active room"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: kind === WidgetKind.Room
|
||||
? _t("See general files posted to this room")
|
||||
: _t("See general files posted to your active room"),
|
||||
};
|
||||
}
|
||||
}
|
||||
default: {
|
||||
let primary: TranslatedString;
|
||||
if (eventCap.direction === EventDirection.Send) {
|
||||
if (kind === WidgetKind.Room) {
|
||||
primary = _t("Send <b>%(msgtype)s</b> messages as you in this room", {
|
||||
msgtype: eventCap.keyStr,
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
});
|
||||
} else {
|
||||
primary = _t("Send <b>%(msgtype)s</b> messages as you in your active room", {
|
||||
msgtype: eventCap.keyStr,
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (kind === WidgetKind.Room) {
|
||||
primary = _t("See <b>%(msgtype)s</b> messages posted to this room", {
|
||||
msgtype: eventCap.keyStr,
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
});
|
||||
} else {
|
||||
primary = _t("See <b>%(msgtype)s</b> messages posted to your active room", {
|
||||
msgtype: eventCap.keyStr,
|
||||
}, {
|
||||
b: sub => <b>{sub}</b>,
|
||||
});
|
||||
}
|
||||
}
|
||||
return {primary};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6532,10 +6532,10 @@ matrix-react-test-utils@^0.2.2:
|
|||
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
|
||||
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
|
||||
|
||||
matrix-widget-api@^0.1.0-beta.8:
|
||||
version "0.1.0-beta.8"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.8.tgz#17e85c03c46353373890b869b1fd46162bdb0026"
|
||||
integrity sha512-sWqyWs0RQqny/BimZUOxUd9BTJBzQmJlJ1i3lsSh1JBygV+aK5xQsONL97fc4i6/nwQPK72uCVDF+HwTtkpAbQ==
|
||||
matrix-widget-api@^0.1.0-beta.9:
|
||||
version "0.1.0-beta.9"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.9.tgz#83952132c1610e013acb3e695f923f971ddd5637"
|
||||
integrity sha512-nXo4iaquSya6hYLXccX8o1K960ckSQ0YXIubRDha+YmB+L09F5a7bUPS5JN2tYANOMzyfFAzWVuFwjHv4+K+rg==
|
||||
dependencies:
|
||||
events "^3.2.0"
|
||||
|
||||
|
|
Loading…
Reference in a new issue