Merge pull request #4618 from matrix-org/t3chguy/toasts3

Migrate Toasts to Typescript and to granular priority system
pull/21833/head
Michael Telatynski 2020-05-26 13:48:47 +01:00 committed by GitHub
commit b7428fcf35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 518 additions and 441 deletions

View File

@ -120,6 +120,7 @@
"@types/classnames": "^2.2.10",
"@types/flux": "^3.1.9",
"@types/modernizr": "^3.5.3",
"@types/node": "^12.12.41",
"@types/qrcode": "^1.3.4",
"@types/react": "16.9",
"@types/zxcvbn": "^4.4.0",

View File

@ -17,6 +17,8 @@ limitations under the License.
import * as ModernizrStatic from "modernizr";
import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore";
import DeviceListener from "../DeviceListener";
declare global {
interface Window {
@ -27,6 +29,8 @@ declare global {
};
mx_ContentMessages: ContentMessages;
mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener;
}
// workaround for https://github.com/microsoft/TypeScript/issues/30933

View File

@ -14,43 +14,43 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClientPeg } from './MatrixClientPeg';
import {MatrixClientPeg} from './MatrixClientPeg';
import SettingsStore from './settings/SettingsStore';
import * as sdk from './index';
import { _t } from './languageHandler';
import ToastStore from './stores/ToastStore';
import {
hideToast as hideBulkUnverifiedSessionsToast,
showToast as showBulkUnverifiedSessionsToast
} from "./toasts/BulkUnverifiedSessionsToast";
import {
hideToast as hideSetupEncryptionToast,
Kind as SetupKind,
Kind,
showToast as showSetupEncryptionToast
} from "./toasts/SetupEncryptionToast";
import {
hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast
} from "./toasts/UnverifiedSessionToast";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
const OTHER_DEVICES_TOAST_KEY = 'reviewsessions';
function toastKey(deviceId) {
return "unverified_session_" + deviceId;
}
export default class DeviceListener {
// device IDs for which the user has dismissed the verify toast ('Later')
private dismissed = new Set<string>();
// has the user dismissed any of the various nag toasts to setup encryption on this device?
private dismissedThisDeviceToast = false;
// cache of the key backup info
private keyBackupInfo: object = null;
private keyBackupFetchedAt: number = null;
// We keep a list of our own device IDs so we can batch ones that were already
// there the last time the app launched into a single toast, but display new
// ones in their own toasts.
private ourDeviceIdsAtStart: Set<string> = null;
// The set of device IDs we're currently displaying toasts for
private displayingToastsForDeviceIds = new Set<string>();
static sharedInstance() {
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
return global.mx_DeviceListener;
}
constructor() {
// device IDs for which the user has dismissed the verify toast ('Later')
this._dismissed = new Set();
// has the user dismissed any of the various nag toasts to setup encryption on this device?
this._dismissedThisDeviceToast = false;
// cache of the key backup info
this._keyBackupInfo = null;
this._keyBackupFetchedAt = null;
// We keep a list of our own device IDs so we can batch ones that were already
// there the last time the app launched into a single toast, but display new
// ones in their own toasts.
this._ourDeviceIdsAtStart = null;
// The set of device IDs we're currently displaying toasts for
this._displayingToastsForDeviceIds = new Set();
if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener();
return window.mx_DeviceListener;
}
start() {
@ -74,12 +74,12 @@ export default class DeviceListener {
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync);
}
this._dismissed.clear();
this._dismissedThisDeviceToast = false;
this._keyBackupInfo = null;
this._keyBackupFetchedAt = null;
this._ourDeviceIdsAtStart = null;
this._displayingToastsForDeviceIds = new Set();
this.dismissed.clear();
this.dismissedThisDeviceToast = false;
this.keyBackupInfo = null;
this.keyBackupFetchedAt = null;
this.ourDeviceIdsAtStart = null;
this.displayingToastsForDeviceIds = new Set();
}
/**
@ -87,29 +87,29 @@ export default class DeviceListener {
*
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/
async dismissUnverifiedSessions(deviceIds) {
async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
for (const d of deviceIds) {
this._dismissed.add(d);
this.dismissed.add(d);
}
this._recheck();
}
dismissEncryptionSetup() {
this._dismissedThisDeviceToast = true;
this.dismissedThisDeviceToast = true;
this._recheck();
}
_ensureDeviceIdsAtStartPopulated() {
if (this._ourDeviceIdsAtStart === null) {
if (this.ourDeviceIdsAtStart === null) {
const cli = MatrixClientPeg.get();
this._ourDeviceIdsAtStart = new Set(
this.ourDeviceIdsAtStart = new Set(
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId),
);
}
}
_onWillUpdateDevices = async (users, initialFetch) => {
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
// If we didn't know about *any* devices before (ie. it's fresh login),
// then they are all pre-existing devices, so ignore this and set the
// devicesAtStart list to the devices that we see after the fetch.
@ -122,17 +122,17 @@ export default class DeviceListener {
// before we download any new ones.
}
_onDevicesUpdated = (users) => {
_onDevicesUpdated = (users: string[]) => {
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
this._recheck();
}
_onDeviceVerificationChanged = (userId) => {
_onDeviceVerificationChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck();
}
_onUserTrustStatusChanged = (userId, trustLevel) => {
_onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck();
}
@ -163,11 +163,11 @@ export default class DeviceListener {
// & cache the result
async _getKeyBackupInfo() {
const now = (new Date()).getTime();
if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this._keyBackupFetchedAt = now;
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.keyBackupFetchedAt = now;
}
return this._keyBackupInfo;
return this.keyBackupInfo;
}
async _recheck() {
@ -186,48 +186,25 @@ export default class DeviceListener {
const crossSigningReady = await cli.isCrossSigningReady();
if (this._dismissedThisDeviceToast) {
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
if (this.dismissedThisDeviceToast || crossSigningReady) {
hideSetupEncryptionToast();
} else {
if (!crossSigningReady) {
// make sure our keys are finished downlaoding
await cli.downloadKeys([cli.getUserId()]);
// cross signing isn't enabled - nag to enable it
// There are 3 different toasts for:
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
// Cross-signing on account but this device doesn't trust the master key (verify this session)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Verify this session"),
icon: "verification_warning",
props: {kind: 'verify_this_session'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
} else {
const backupInfo = await this._getKeyBackupInfo();
if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Encryption upgrade available"),
icon: "verification_warning",
props: {kind: 'upgrade_encryption'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
} else {
// No cross-signing or key backup on account (set up encryption)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Set up encryption"),
icon: "verification_warning",
props: {kind: 'set_up_encryption'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
});
}
}
// make sure our keys are finished downloading
await cli.downloadKeys([cli.getUserId()]);
// cross signing isn't enabled - nag to enable it
// There are 3 different toasts for:
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
// Cross-signing on account but this device doesn't trust the master key (verify this session)
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
} else {
// cross-signing is ready, and we don't need to upgrade encryption
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
const backupInfo = await this._getKeyBackupInfo();
if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
showSetupEncryptionToast(Kind.UPGRADE_ENCRYPTION);
} else {
// No cross-signing or key backup on account (set up encryption)
showSetupEncryptionToast(Kind.SET_UP_ENCRYPTION);
}
}
}
@ -239,20 +216,20 @@ export default class DeviceListener {
// (technically could just be a boolean: we don't actually
// need to remember the device IDs, but for the sake of
// symmetry...).
const oldUnverifiedDeviceIds = new Set();
const oldUnverifiedDeviceIds = new Set<string>();
// Unverified devices that have appeared since then
const newUnverifiedDeviceIds = new Set();
const newUnverifiedDeviceIds = new Set<string>();
// as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts
if (crossSigningReady) {
const devices = cli.getStoredDevicesForUser(cli.getUserId());
for (const device of devices) {
if (device.deviceId == cli.deviceId) continue;
if (device.deviceId === cli.deviceId) continue;
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) {
if (this._ourDeviceIdsAtStart.has(device.deviceId)) {
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
oldUnverifiedDeviceIds.add(device.deviceId);
} else {
newUnverifiedDeviceIds.add(device.deviceId);
@ -263,38 +240,23 @@ export default class DeviceListener {
// Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) {
ToastStore.sharedInstance().addOrReplaceToast({
key: OTHER_DEVICES_TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
priority: ToastStore.PRIORITY_LOW,
props: {
deviceIds: oldUnverifiedDeviceIds,
},
component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"),
});
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
} else {
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY);
hideBulkUnverifiedSessionsToast();
}
// Show toasts for new unverified devices if they aren't already there
for (const deviceId of newUnverifiedDeviceIds) {
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: { deviceId },
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
});
showUnverifiedSessionsToast(deviceId);
}
// ...and hide any we don't need any more
for (const deviceId of this._displayingToastsForDeviceIds) {
for (const deviceId of this.displayingToastsForDeviceIds) {
if (!newUnverifiedDeviceIds.has(deviceId)) {
ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
hideUnverifiedSessionsToast(deviceId);
}
}
this._displayingToastsForDeviceIds = newUnverifiedDeviceIds;
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
}
}

View File

@ -1570,7 +1570,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
icon: "verification",
props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"),
priority: ToastStore.PRIORITY_REALTIME,
priority: 90,
});
}
});

View File

@ -15,13 +15,21 @@ limitations under the License.
*/
import * as React from "react";
import ToastStore from "../../stores/ToastStore";
import ToastStore, {IToast} from "../../stores/ToastStore";
import classNames from "classnames";
export default class ToastContainer extends React.Component {
constructor() {
super();
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
interface IState {
toasts: IToast<any>[];
countSeen: number;
}
export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) {
super(props, context);
this.state = {
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
};
// Start listening here rather than in componentDidMount because
// toasts may dismiss themselves in their didMount if they find
@ -35,7 +43,10 @@ export default class ToastContainer extends React.Component {
}
_onToastStoreUpdate = () => {
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
this.setState({
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
});
};
render() {
@ -51,8 +62,8 @@ export default class ToastContainer extends React.Component {
});
let countIndicator;
if (isStacked) {
countIndicator = `(1/${totalCount})`;
if (isStacked || this.state.countSeen > 0) {
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
}
const toastProps = Object.assign({}, props, {

View File

@ -1,56 +0,0 @@
/*
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from "../../../dispatcher/dispatcher";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.BulkUnverifiedSessionsToast")
export default class BulkUnverifiedSessionsToast extends React.PureComponent {
static propTypes = {
deviceIds: PropTypes.array,
}
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
};
_onReviewClick = async () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
});
};
render() {
return (<div>
<div className="mx_Toast_description">
{_t("Verify all your sessions to ensure your account & messages are safe")}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Review")} onClick={this._onReviewClick} />
</div>
</div>);
}
}

View File

@ -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.
*/
import React, {ReactChild} from "react";
import FormButton from "../elements/FormButton";
interface IProps {
description: ReactChild;
acceptLabel: string;
rejectLabel?: string;
onAccept();
onReject?();
}
const GenericToast: React.FC<IProps> = ({description, acceptLabel, rejectLabel, onAccept, onReject}) => {
return <div>
<div className="mx_Toast_description">
{ description }
</div>
<div className="mx_Toast_buttons" aria-live="off">
{onReject && rejectLabel && <FormButton label={rejectLabel} kind="danger" onClick={onReject} /> }
<FormButton label={acceptLabel} onClick={onAccept} />
</div>
</div>;
};
export default GenericToast;

View File

@ -1,88 +0,0 @@
/*
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 PropTypes from 'prop-types';
import Modal from '../../../Modal';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import DeviceListener from '../../../DeviceListener';
import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from '../../../CrossSigningManager';
export default class SetupEncryptionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
kind: PropTypes.oneOf([
'set_up_encryption',
'verify_this_session',
'upgrade_encryption',
]).isRequired,
};
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
_onSetupClick = async () => {
if (this.props.kind === "verify_this_session") {
Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(
Spinner, null, 'mx_Dialog_spinner', /* priority */ false, /* static */ true,
);
try {
await accessSecretStorage();
} finally {
modal.close();
}
}
};
getDescription() {
switch (this.props.kind) {
case 'set_up_encryption':
case 'upgrade_encryption':
return _t('Verify yourself & others to keep your chats safe');
case 'verify_this_session':
return _t('Other users may not trust it');
}
}
getSetupCaption() {
switch (this.props.kind) {
case 'set_up_encryption':
return _t('Set up');
case 'upgrade_encryption':
return _t('Upgrade');
case 'verify_this_session':
return _t('Verify');
}
}
render() {
const FormButton = sdk.getComponent("elements.FormButton");
return (<div>
<div className="mx_Toast_description">{this.getDescription()}</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={this.getSetupCaption()} onClick={this._onSetupClick} />
</div>
</div>);
}
}

View File

@ -1,66 +0,0 @@
/*
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import DeviceListener from '../../../DeviceListener';
import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.UnverifiedSessionToast")
export default class UnverifiedSessionToast extends React.PureComponent {
static propTypes = {
deviceId: PropTypes.string,
}
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
};
_onReviewClick = async () => {
const cli = MatrixClientPeg.get();
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), this.props.deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
}
},
}, null, /* priority = */ false, /* static = */ true);
};
render() {
const cli = MatrixClientPeg.get();
const device = cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
return (<div>
<div className="mx_Toast_description">
{_t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()})}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Verify")} onClick={this._onReviewClick} />
</div>
</div>);
}
}

View File

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import React from "react";
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -24,8 +24,23 @@ import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver
import dis from "../../../dispatcher/dispatcher";
import ToastStore from "../../../stores/ToastStore";
import Modal from "../../../Modal";
import GenericToast from "./GenericToast";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo";
interface IProps {
toastKey: string;
request: VerificationRequest;
}
interface IState {
counter: number;
device?: DeviceInfo;
}
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
private intervalHandle: NodeJS.Timeout;
export default class VerificationRequestToast extends React.PureComponent {
constructor(props) {
super(props);
this.state = {counter: Math.ceil(props.request.timeout / 1000)};
@ -34,7 +49,7 @@ export default class VerificationRequestToast extends React.PureComponent {
async componentDidMount() {
const {request} = this.props;
if (request.timeout && request.timeout > 0) {
this._intervalHandle = setInterval(() => {
this.intervalHandle = setInterval(() => {
let {counter} = this.state;
counter = Math.max(0, counter - 1);
this.setState({counter});
@ -56,7 +71,7 @@ export default class VerificationRequestToast extends React.PureComponent {
}
componentWillUnmount() {
clearInterval(this._intervalHandle);
clearInterval(this.intervalHandle);
const {request} = this.props;
request.off("change", this._checkRequestIsPending);
}
@ -110,7 +125,6 @@ export default class VerificationRequestToast extends React.PureComponent {
};
render() {
const FormButton = sdk.getComponent("elements.FormButton");
const {request} = this.props;
let nameLabel;
if (request.isSelfVerification) {
@ -133,20 +147,16 @@ export default class VerificationRequestToast extends React.PureComponent {
}
}
}
const declineLabel = this.state.counter == 0 ?
const declineLabel = this.state.counter === 0 ?
_t("Decline") :
_t("Decline (%(counter)s)", {counter: this.state.counter});
return (<div>
<div className="mx_Toast_description">{nameLabel}</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={declineLabel} kind="danger" onClick={this.cancel} />
<FormButton label={_t("Accept")} onClick={this.accept} />
</div>
</div>);
return <GenericToast
description={nameLabel}
acceptLabel={_t("Accept")}
onAccept={this.accept}
rejectLabel={declineLabel}
onReject={this.cancel}
/>;
}
}
VerificationRequestToast.propTypes = {
request: PropTypes.object.isRequired,
toastKey: PropTypes.string.isRequired,
};

View File

@ -102,11 +102,6 @@
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"Verify this session": "Verify this session",
"Encryption upgrade available": "Encryption upgrade available",
"Set up encryption": "Set up encryption",
"Review where youre logged in": "Review where youre logged in",
"New login. Was this you?": "New login. Was this you?",
"Who would you like to add to this community?": "Who would you like to add to this community?",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
"Invite new community members": "Invite new community members",
@ -396,6 +391,20 @@
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"Review where youre logged in": "Review where youre logged in",
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
"Review": "Review",
"Later": "Later",
"Set up encryption": "Set up encryption",
"Encryption upgrade available": "Encryption upgrade available",
"Verify this session": "Verify this session",
"Set up": "Set up",
"Upgrade": "Upgrade",
"Verify": "Verify",
"Verify yourself & others to keep your chats safe": "Verify yourself & others to keep your chats safe",
"Other users may not trust it": "Other users may not trust it",
"New login. Was this you?": "New login. Was this you?",
"Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s",
"There was an error joining the room": "There was an error joining the room",
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
@ -570,15 +579,6 @@
"Headphones": "Headphones",
"Folder": "Folder",
"Pin": "Pin",
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
"Later": "Later",
"Review": "Review",
"Verify yourself & others to keep your chats safe": "Verify yourself & others to keep your chats safe",
"Other users may not trust it": "Other users may not trust it",
"Set up": "Set up",
"Upgrade": "Upgrade",
"Verify": "Verify",
"Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s",
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
"Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",

View File

@ -1,73 +0,0 @@
/*
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 EventEmitter from 'events';
/**
* Holds the active toasts
*/
export default class ToastStore extends EventEmitter {
static PRIORITY_REALTIME = 0;
static PRIORITY_DEFAULT = 1;
static PRIORITY_LOW = 2;
static sharedInstance() {
if (!global.mx_ToastStore) global.mx_ToastStore = new ToastStore();
return global.mx_ToastStore;
}
constructor() {
super();
this._dispatcherRef = null;
this._toasts = [];
}
reset() {
this._toasts = [];
}
/**
* Add or replace a toast
* If a toast with the same toastKey already exists, the given toast will replace it
* Toasts are always added underneath any toasts of the same priority, so existing
* toasts stay at the top unless a higher priority one arrives (better to not change the
* toast unless necessary).
*
* @param {boject} newToast The new toast
*/
addOrReplaceToast(newToast) {
if (newToast.priority === undefined) newToast.priority = ToastStore.PRIORITY_DEFAULT;
const oldIndex = this._toasts.findIndex(t => t.key === newToast.key);
if (oldIndex === -1) {
let newIndex = this._toasts.length;
while (newIndex > 0 && this._toasts[newIndex - 1].priority > newToast.priority) --newIndex;
this._toasts.splice(newIndex, 0, newToast);
} else {
this._toasts[oldIndex] = newToast;
}
this.emit('update');
}
dismissToast(key) {
this._toasts = this._toasts.filter(t => t.key !== key);
this.emit('update');
}
getToasts() {
return this._toasts;
}
}

91
src/stores/ToastStore.ts Normal file
View File

@ -0,0 +1,91 @@
/*
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 EventEmitter from "events";
import React, {JSXElementConstructor} from "react";
export interface IToast<C extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> {
key: string;
// higher priority number will be shown on top of lower priority
priority: number;
title: string;
icon?: string;
component: C;
props?: React.ComponentProps<C>;
}
/**
* Holds the active toasts
*/
export default class ToastStore extends EventEmitter {
private toasts: IToast<any>[] = [];
// The count of toasts which have been seen & dealt with in this stack
// where the count resets when the stack of toasts clears.
private countSeen = 0;
static sharedInstance() {
if (!window.mx_ToastStore) window.mx_ToastStore = new ToastStore();
return window.mx_ToastStore;
}
reset() {
this.toasts = [];
this.countSeen = 0;
}
/**
* Add or replace a toast
* If a toast with the same toastKey already exists, the given toast will replace it
* Toasts are always added underneath any toasts of the same priority, so existing
* toasts stay at the top unless a higher priority one arrives (better to not change the
* toast unless necessary).
*
* @param {object} newToast The new toast
*/
addOrReplaceToast<C extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>>(newToast: IToast<C>) {
const oldIndex = this.toasts.findIndex(t => t.key === newToast.key);
if (oldIndex === -1) {
let newIndex = this.toasts.length;
while (newIndex > 0 && this.toasts[newIndex - 1].priority < newToast.priority) --newIndex;
this.toasts.splice(newIndex, 0, newToast);
} else {
this.toasts[oldIndex] = newToast;
}
this.emit('update');
}
dismissToast(key) {
const length = this.toasts.length;
this.toasts = this.toasts.filter(t => t.key !== key);
if (length !== this.toasts.length) {
if (this.toasts.length === 0) {
this.countSeen = 0;
} else {
this.countSeen++;
}
this.emit('update');
}
}
getToasts() {
return this.toasts;
}
getCountSeen() {
return this.countSeen;
}
}

View File

@ -0,0 +1,58 @@
/*
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 { _t } from '../languageHandler';
import dis from "../dispatcher/dispatcher";
import { MatrixClientPeg } from '../MatrixClientPeg';
import DeviceListener from '../DeviceListener';
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const TOAST_KEY = "reviewsessions";
export const showToast = (deviceIds: Set<string>) => {
const onAccept = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
});
};
const onReject = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
};
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
props: {
description: _t("Verify all your sessions to ensure your account & messages are safe"),
acceptLabel: _t("Review"),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: 50,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View File

@ -0,0 +1,106 @@
/*
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 Modal from "../Modal";
import * as sdk from "../index";
import { _t } from "../languageHandler";
import DeviceListener from "../DeviceListener";
import SetupEncryptionDialog from "../components/views/dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from "../CrossSigningManager";
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
const TOAST_KEY = "setupencryption";
const getTitle = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
return _t("Set up encryption");
case Kind.UPGRADE_ENCRYPTION:
return _t("Encryption upgrade available");
case Kind.VERIFY_THIS_SESSION:
return _t("Verify this session");
}
};
const getSetupCaption = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
return _t("Set up");
case Kind.UPGRADE_ENCRYPTION:
return _t("Upgrade");
case Kind.VERIFY_THIS_SESSION:
return _t("Verify");
}
};
const getDescription = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
case Kind.UPGRADE_ENCRYPTION:
return _t("Verify yourself & others to keep your chats safe");
case Kind.VERIFY_THIS_SESSION:
return _t("Other users may not trust it");
}
};
export enum Kind {
SET_UP_ENCRYPTION = "set_up_encryption",
UPGRADE_ENCRYPTION = "upgrade_encryption",
VERIFY_THIS_SESSION = "verify_this_session",
}
const onReject = () => {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
export const showToast = (kind: Kind) => {
const onAccept = async () => {
if (kind === Kind.VERIFY_THIS_SESSION) {
Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog,
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(
Spinner, null, "mx_Dialog_spinner", /* priority */ false, /* static */ true,
);
try {
await accessSecretStorage();
} finally {
modal.close();
}
}
};
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: getTitle(kind),
icon: "verification_warning",
props: {
description: getDescription(kind),
acceptLabel: getSetupCaption(kind),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View File

@ -0,0 +1,70 @@
/*
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 { _t } from '../languageHandler';
import { MatrixClientPeg } from '../MatrixClientPeg';
import Modal from '../Modal';
import DeviceListener from '../DeviceListener';
import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
function toastKey(deviceId: string) {
return "unverified_session_" + deviceId;
}
export const showToast = (deviceId: string) => {
const cli = MatrixClientPeg.get();
const onAccept = () => {
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
}
},
}, null, /* priority = */ false, /* static = */ true);
};
const onReject = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
};
const device = cli.getStoredDevice(cli.getUserId(), deviceId);
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: {
description: _t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}),
acceptLabel: _t("Verify"),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: 80,
});
};
export const hideToast = (deviceId: string) => {
ToastStore.sharedInstance().dismissToast(deviceId);
};

View File

@ -1280,6 +1280,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b"
integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==
"@types/node@^12.12.41":
version "12.12.42"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.42.tgz#d0d1149336bd07540dd1ea576692829d575dec34"
integrity sha512-R/9QdYFLL9dE9l5cWWzWIZByVGFd7lk7JVOJ7KD+E1SJ4gni7XJRLz9QTjyYQiHIqEAgku9VgxdLjMlhhUaAFg==
"@types/prop-types@*":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"