mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-17 14:05:04 +08:00
Merge pull request #4506 from matrix-org/dbkr/aggregate_device_verify_toasts
Aggregate device verify toasts
This commit is contained in:
commit
60d51a0f1e
@ -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 you’re logged in"),
|
||||||
this._activeNagToasts = newActiveToasts;
|
icon: "verification_warning",
|
||||||
|
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 = () => {
|
||||||
|
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||||
|
// things are in... this knows far more than it should do about the state of the rest
|
||||||
|
// 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({
|
dis.dispatch({
|
||||||
action: "view_user",
|
action: "view_user",
|
||||||
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
|
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
|
||||||
|
this.state.member : null,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
panel = <UserInfo
|
panel = <UserInfo
|
||||||
user={this.state.member}
|
user={this.state.member}
|
||||||
|
@ -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,6 +189,17 @@ 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");
|
||||||
|
|
||||||
|
|
||||||
|
if (isVerified) {
|
||||||
|
return (
|
||||||
|
<div className={classes} title={device.deviceId} >
|
||||||
|
<div className={iconClasses} />
|
||||||
|
<div className="mx_UserInfo_device_name">{deviceName}</div>
|
||||||
|
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className={classes}
|
className={classes}
|
||||||
@ -203,6 +212,7 @@ function DeviceItem({userId, device}) {
|
|||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function DevicesSection({devices, userId, loading}) {
|
function DevicesSection({devices, userId, loading}) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -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} />
|
||||||
|
@ -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 you’re logged in": "Review where you’re 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",
|
||||||
@ -564,6 +564,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)",
|
||||||
|
@ -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,6 +69,13 @@ export async function verifyDevice(user, device) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.userId === cli.getUserId()) {
|
||||||
|
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
|
||||||
|
userId: user.userId,
|
||||||
|
device,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
Modal.createTrackedDialog("Verification warning", "unverified session", UntrustedDeviceDialog, {
|
Modal.createTrackedDialog("Verification warning", "unverified session", UntrustedDeviceDialog, {
|
||||||
user,
|
user,
|
||||||
device,
|
device,
|
||||||
@ -84,7 +92,8 @@ export async function verifyDevice(user, device) {
|
|||||||
refireParams: {member: user, verificationRequestPromise},
|
refireParams: {member: user, verificationRequestPromise},
|
||||||
});
|
});
|
||||||
} else if (action === "legacy") {
|
} else if (action === "legacy") {
|
||||||
const ManualDeviceKeyVerificationDialog = sdk.getComponent("dialogs.ManualDeviceKeyVerificationDialog");
|
const ManualDeviceKeyVerificationDialog =
|
||||||
|
sdk.getComponent("dialogs.ManualDeviceKeyVerificationDialog");
|
||||||
Modal.createTrackedDialog("Legacy verify session", "legacy verify session",
|
Modal.createTrackedDialog("Legacy verify session", "legacy verify session",
|
||||||
ManualDeviceKeyVerificationDialog,
|
ManualDeviceKeyVerificationDialog,
|
||||||
{
|
{
|
||||||
@ -96,6 +105,7 @@ export async function verifyDevice(user, device) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function legacyVerifyUser(user) {
|
export async function legacyVerifyUser(user) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
Loading…
Reference in New Issue
Block a user