Merge pull request #4516 from matrix-org/dbkr/aggregate_device_verify_toasts_rel

Aggregate device verify toasts
This commit is contained in:
David Baker 2020-04-29 12:49:50 +01:00 committed by GitHub
commit dfc1c5e627
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 110 additions and 98 deletions

View file

@ -20,12 +20,9 @@ import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import ToastStore from './stores/ToastStore'; import ToastStore from './stores/ToastStore';
function toastKey(deviceId) {
return 'unverified_session_' + deviceId;
}
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
const THIS_DEVICE_TOAST_KEY = 'setupencryption'; const THIS_DEVICE_TOAST_KEY = 'setupencryption';
const OTHER_DEVICES_TOAST_KEY = 'reviewsessions';
export default class DeviceListener { export default class DeviceListener {
static sharedInstance() { static sharedInstance() {
@ -34,8 +31,6 @@ export default class DeviceListener {
} }
constructor() { constructor() {
// set of device IDs we're currently showing toasts for
this._activeNagToasts = new Set();
// device IDs for which the user has dismissed the verify toast ('Later') // device IDs for which the user has dismissed the verify toast ('Later')
this._dismissed = new Set(); this._dismissed = new Set();
// has the user dismissed any of the various nag toasts to setup encryption on this device? // has the user dismissed any of the various nag toasts to setup encryption on this device?
@ -71,8 +66,11 @@ export default class DeviceListener {
this._keyBackupFetchedAt = null; this._keyBackupFetchedAt = null;
} }
dismissVerification(deviceId) { async dismissVerifications() {
this._dismissed.add(deviceId); const cli = MatrixClientPeg.get();
const devices = await cli.getStoredDevicesForUser(cli.getUserId());
this._dismissed = new Set(devices.filter(d => d.deviceId !== cli.deviceId).map(d => d.deviceId));
this._recheck(); this._recheck();
} }
@ -202,33 +200,29 @@ export default class DeviceListener {
// as long as cross-signing isn't ready, // as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts // you can't see or dismiss any device toasts
if (crossSigningReady) { if (crossSigningReady) {
const newActiveToasts = new Set(); let haveUnverifiedDevices = false;
const devices = await cli.getStoredDevicesForUser(cli.getUserId()); const devices = await cli.getStoredDevicesForUser(cli.getUserId());
for (const device of devices) { 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); const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) { if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) {
ToastStore.sharedInstance().dismissToast(toastKey(device.deviceId)); haveUnverifiedDevices = true;
} else { break;
this._activeNagToasts.add(device.deviceId);
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(device.deviceId),
title: _t("Unverified login. Was this you?"),
icon: "verification_warning",
props: { device },
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
});
newActiveToasts.add(device.deviceId);
} }
} }
// clear any other outstanding toasts (eg. logged out devices) if (haveUnverifiedDevices) {
for (const deviceId of this._activeNagToasts) { ToastStore.sharedInstance().addOrReplaceToast({
if (!newActiveToasts.has(deviceId)) ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); key: OTHER_DEVICES_TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
});
} else {
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY);
} }
this._activeNagToasts = newActiveToasts;
} }
} }
} }

View file

@ -221,10 +221,27 @@ export default class RightPanel extends React.Component {
case RIGHT_PANEL_PHASES.EncryptionPanel: case RIGHT_PANEL_PHASES.EncryptionPanel:
if (SettingsStore.getValue("feature_cross_signing")) { if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => { const onClose = () => {
dis.dispatch({ // XXX: There are three different ways of 'closing' this panel depending on what state
action: "view_user", // things are in... this knows far more than it should do about the state of the rest
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null, // of the app and is generally a bit silly.
}); if (this.props.user) {
// If we have a user prop then we're displaying a user from the 'user' page type
// in LoggedInView, so need to change the page type to close the panel (we switch
// to the home page which is not obviosuly the correct thing to do, but I'm not sure
// anything else is - we could hide the close button altogether?)
dis.dispatch({
action: "view_home_page",
});
} else {
// Otherwise we have got our user from RoomViewStore which means we're being shown
// within a room, so go back to the member panel if we were in the encryption panel,
// or the member list if we were in the member panel... phew.
dis.dispatch({
action: "view_user",
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
this.state.member : null,
});
}
}; };
panel = <UserInfo panel = <UserInfo
user={this.state.member} user={this.state.member}

View file

@ -181,9 +181,7 @@ function DeviceItem({userId, device}) {
}); });
const onDeviceClick = () => { const onDeviceClick = () => {
if (!isVerified) { verifyDevice(cli.getUser(userId), device);
verifyDevice(cli.getUser(userId), device);
}
}; };
const deviceName = device.ambiguous ? const deviceName = device.ambiguous ?
@ -191,17 +189,29 @@ function DeviceItem({userId, device}) {
device.getDisplayName(); device.getDisplayName();
let trustedLabel = null; let trustedLabel = null;
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
return (
<AccessibleButton
className={classes} if (isVerified) {
title={device.deviceId} return (
onClick={onDeviceClick} <div className={classes} title={device.deviceId} >
> <div className={iconClasses} />
<div className={iconClasses} /> <div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_name">{deviceName}</div> <div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div> </div>
</AccessibleButton> );
); } else {
return (
<AccessibleButton
className={classes}
title={device.deviceId}
onClick={onDeviceClick}
>
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
</AccessibleButton>
);
}
} }
function DevicesSection({devices, userId, loading}) { function DevicesSection({devices, userId, loading}) {

View file

@ -15,52 +15,32 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from "../../../Modal"; import dis from "../../../dispatcher";
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener'; import DeviceListener from '../../../DeviceListener';
import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
import FormButton from '../elements/FormButton'; import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent'; import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.UnverifiedSessionToast") @replaceableComponent("views.toasts.UnverifiedSessionToast")
export default class UnverifiedSessionToast extends React.PureComponent { export default class UnverifiedSessionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
};
_onLaterClick = () => { _onLaterClick = () => {
const { device } = this.props; DeviceListener.sharedInstance().dismissVerifications();
DeviceListener.sharedInstance().dismissVerification(device.deviceId);
}; };
_onReviewClick = async () => { _onReviewClick = async () => {
const { device } = this.props; DeviceListener.sharedInstance().dismissVerifications();
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, { dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(), userId: MatrixClientPeg.get().getUserId(),
device, });
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
this._onLaterClick();
}
},
}, null, /* priority = */ false, /* static = */ true);
}; };
render() { render() {
const { device } = this.props;
return (<div> return (<div>
<div className="mx_Toast_description"> <div className="mx_Toast_description">
<span className="mx_Toast_deviceName"> {_t("Verify your other sessions")}
{device.getDisplayName()}
</span> <span className="mx_Toast_deviceID">
({device.deviceId})
</span>
</div> </div>
<div className="mx_Toast_buttons" aria-live="off"> <div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} /> <FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />

View file

@ -105,7 +105,7 @@
"Verify this session": "Verify this session", "Verify this session": "Verify this session",
"Encryption upgrade available": "Encryption upgrade available", "Encryption upgrade available": "Encryption upgrade available",
"Set up encryption": "Set up encryption", "Set up encryption": "Set up encryption",
"Unverified login. Was this you?": "Unverified login. Was this you?", "Review where youre logged in": "Review where youre logged in",
"Who would you like to add to this community?": "Who would you like to add to this community?", "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", "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", "Invite new community members": "Invite new community members",
@ -562,6 +562,7 @@
"Upgrade": "Upgrade", "Upgrade": "Upgrade",
"Verify": "Verify", "Verify": "Verify",
"Later": "Later", "Later": "Later",
"Verify your other sessions": "Verify your other sessions",
"Review": "Review", "Review": "Review",
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)", "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
"Decline (%(counter)s)": "Decline (%(counter)s)", "Decline (%(counter)s)": "Decline (%(counter)s)",

View file

@ -23,6 +23,7 @@ import {RIGHT_PANEL_PHASES} from "./stores/RightPanelStorePhases";
import {findDMForUser} from './createRoom'; import {findDMForUser} from './createRoom';
import {accessSecretStorage} from './CrossSigningManager'; import {accessSecretStorage} from './CrossSigningManager';
import SettingsStore from './settings/SettingsStore'; import SettingsStore from './settings/SettingsStore';
import NewSessionReviewDialog from './components/views/dialogs/NewSessionReviewDialog';
import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {verificationMethods} from 'matrix-js-sdk/src/crypto';
async function enable4SIfNeeded() { async function enable4SIfNeeded() {
@ -68,33 +69,42 @@ export async function verifyDevice(user, device) {
return; return;
} }
} }
Modal.createTrackedDialog("Verification warning", "unverified session", UntrustedDeviceDialog, {
user, if (user.userId === cli.getUserId()) {
device, Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
onFinished: async (action) => { userId: user.userId,
if (action === "sas") { device,
const verificationRequestPromise = cli.legacyDeviceVerification( });
user.userId, } else {
device.deviceId, Modal.createTrackedDialog("Verification warning", "unverified session", UntrustedDeviceDialog, {
verificationMethods.SAS, user,
); device,
dis.dispatch({ onFinished: async (action) => {
action: "set_right_panel_phase", if (action === "sas") {
phase: RIGHT_PANEL_PHASES.EncryptionPanel, const verificationRequestPromise = cli.legacyDeviceVerification(
refireParams: {member: user, verificationRequestPromise}, user.userId,
}); device.deviceId,
} else if (action === "legacy") { verificationMethods.SAS,
const ManualDeviceKeyVerificationDialog = sdk.getComponent("dialogs.ManualDeviceKeyVerificationDialog"); );
Modal.createTrackedDialog("Legacy verify session", "legacy verify session", dis.dispatch({
ManualDeviceKeyVerificationDialog, action: "set_right_panel_phase",
{ phase: RIGHT_PANEL_PHASES.EncryptionPanel,
userId: user.userId, refireParams: {member: user, verificationRequestPromise},
device, });
}, } else if (action === "legacy") {
); const ManualDeviceKeyVerificationDialog =
} sdk.getComponent("dialogs.ManualDeviceKeyVerificationDialog");
}, Modal.createTrackedDialog("Legacy verify session", "legacy verify session",
}); ManualDeviceKeyVerificationDialog,
{
userId: user.userId,
device,
},
);
}
},
});
}
} }
export async function legacyVerifyUser(user) { export async function legacyVerifyUser(user) {