diff --git a/src/DeviceListener.js b/src/DeviceListener.js index 41f249b335..1b451310b9 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.js @@ -20,12 +20,9 @@ import * as sdk from './index'; import { _t } from './languageHandler'; import ToastStore from './stores/ToastStore'; -function toastKey(deviceId) { - return 'unverified_session_' + deviceId; -} - const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; const THIS_DEVICE_TOAST_KEY = 'setupencryption'; +const OTHER_DEVICES_TOAST_KEY = 'reviewsessions'; export default class DeviceListener { static sharedInstance() { @@ -34,8 +31,6 @@ export default class DeviceListener { } 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') this._dismissed = new Set(); // 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; } - dismissVerification(deviceId) { - this._dismissed.add(deviceId); + async dismissVerifications() { + 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(); } @@ -202,33 +200,29 @@ export default class DeviceListener { // as long as cross-signing isn't ready, // you can't see or dismiss any device toasts if (crossSigningReady) { - const newActiveToasts = new Set(); + let haveUnverifiedDevices = false; const devices = await cli.getStoredDevicesForUser(cli.getUserId()); for (const device of devices) { if (device.deviceId == cli.deviceId) continue; const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId); - if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) { - ToastStore.sharedInstance().dismissToast(toastKey(device.deviceId)); - } else { - 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); + if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) { + haveUnverifiedDevices = true; + break; } } - // clear any other outstanding toasts (eg. logged out devices) - for (const deviceId of this._activeNagToasts) { - if (!newActiveToasts.has(deviceId)) ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); + if (haveUnverifiedDevices) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: OTHER_DEVICES_TOAST_KEY, + title: _t("Review where you’re logged in"), + icon: "verification_warning", + component: sdk.getComponent("toasts.UnverifiedSessionToast"), + }); + } else { + ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY); } - this._activeNagToasts = newActiveToasts; } } } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index f5bdfdf40d..3fec5aa25f 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -221,10 +221,27 @@ export default class RightPanel extends React.Component { case RIGHT_PANEL_PHASES.EncryptionPanel: if (SettingsStore.getValue("feature_cross_signing")) { const onClose = () => { - dis.dispatch({ - action: "view_user", - member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null, - }); + // 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({ + action: "view_user", + member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? + this.state.member : null, + }); + } }; panel = { - if (!isVerified) { - verifyDevice(cli.getUser(userId), device); - } + verifyDevice(cli.getUser(userId), device); }; const deviceName = device.ambiguous ? @@ -191,17 +189,29 @@ function DeviceItem({userId, device}) { device.getDisplayName(); let trustedLabel = null; if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted"); - return ( - -
-
{deviceName}
-
{trustedLabel}
- - ); + + + if (isVerified) { + return ( +
+
+
{deviceName}
+
{trustedLabel}
+
+ ); + } else { + return ( + +
+
{deviceName}
+
{trustedLabel}
+ + ); + } } function DevicesSection({devices, userId, loading}) { diff --git a/src/components/views/toasts/UnverifiedSessionToast.js b/src/components/views/toasts/UnverifiedSessionToast.js index cb0cadcdc8..886e3c4c20 100644 --- a/src/components/views/toasts/UnverifiedSessionToast.js +++ b/src/components/views/toasts/UnverifiedSessionToast.js @@ -15,52 +15,32 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -import Modal from "../../../Modal"; +import dis from "../../../dispatcher"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; 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 = { - toastKey: PropTypes.string.isRequired, - device: PropTypes.object.isRequired, - }; - _onLaterClick = () => { - const { device } = this.props; - DeviceListener.sharedInstance().dismissVerification(device.deviceId); + DeviceListener.sharedInstance().dismissVerifications(); }; _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(), - 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() { - const { device } = this.props; - return (
- - {device.getDisplayName()} - - ({device.deviceId}) - + {_t("Verify your other sessions")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4655ea1f14..a308f91748 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -105,7 +105,7 @@ "Verify this session": "Verify this session", "Encryption upgrade available": "Encryption upgrade available", "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?", "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", @@ -564,6 +564,7 @@ "Upgrade": "Upgrade", "Verify": "Verify", "Later": "Later", + "Verify your other sessions": "Verify your other sessions", "Review": "Review", "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)", "Decline (%(counter)s)": "Decline (%(counter)s)", diff --git a/src/verification.js b/src/verification.js index ca839940e5..630da01091 100644 --- a/src/verification.js +++ b/src/verification.js @@ -23,6 +23,7 @@ import {RIGHT_PANEL_PHASES} from "./stores/RightPanelStorePhases"; import {findDMForUser} from './createRoom'; import {accessSecretStorage} from './CrossSigningManager'; import SettingsStore from './settings/SettingsStore'; +import NewSessionReviewDialog from './components/views/dialogs/NewSessionReviewDialog'; import {verificationMethods} from 'matrix-js-sdk/src/crypto'; async function enable4SIfNeeded() { @@ -68,33 +69,42 @@ export async function verifyDevice(user, device) { return; } } - Modal.createTrackedDialog("Verification warning", "unverified session", UntrustedDeviceDialog, { - user, - device, - onFinished: async (action) => { - if (action === "sas") { - const verificationRequestPromise = cli.legacyDeviceVerification( - user.userId, - device.deviceId, - verificationMethods.SAS, - ); - dis.dispatch({ - action: "set_right_panel_phase", - phase: RIGHT_PANEL_PHASES.EncryptionPanel, - refireParams: {member: user, verificationRequestPromise}, - }); - } else if (action === "legacy") { - const ManualDeviceKeyVerificationDialog = sdk.getComponent("dialogs.ManualDeviceKeyVerificationDialog"); - Modal.createTrackedDialog("Legacy verify session", "legacy verify session", - ManualDeviceKeyVerificationDialog, - { - userId: user.userId, - device, - }, - ); - } - }, - }); + + 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, { + user, + device, + onFinished: async (action) => { + if (action === "sas") { + const verificationRequestPromise = cli.legacyDeviceVerification( + user.userId, + device.deviceId, + verificationMethods.SAS, + ); + dis.dispatch({ + action: "set_right_panel_phase", + phase: RIGHT_PANEL_PHASES.EncryptionPanel, + refireParams: {member: user, verificationRequestPromise}, + }); + } 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) {