Merge pull request #4516 from matrix-org/dbkr/aggregate_device_verify_toasts_rel
Aggregate device verify toastspull/21833/head
						commit
						dfc1c5e627
					
				|  | @ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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 = <UserInfo | ||||
|                         user={this.state.member} | ||||
|  |  | |||
|  | @ -181,9 +181,7 @@ function DeviceItem({userId, device}) { | |||
|     }); | ||||
| 
 | ||||
|     const onDeviceClick = () => { | ||||
|         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 ( | ||||
|         <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> | ||||
|     ); | ||||
| 
 | ||||
| 
 | ||||
|     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 ( | ||||
|             <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}) { | ||||
|  |  | |||
|  | @ -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 (<div> | ||||
|             <div className="mx_Toast_description"> | ||||
|                 <span className="mx_Toast_deviceName"> | ||||
|                     {device.getDisplayName()} | ||||
|                 </span> <span className="mx_Toast_deviceID"> | ||||
|                     ({device.deviceId}) | ||||
|                 </span> | ||||
|                 {_t("Verify your other sessions")} | ||||
|             </div> | ||||
|             <div className="mx_Toast_buttons" aria-live="off"> | ||||
|                 <FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} /> | ||||
|  |  | |||
|  | @ -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", | ||||
|  | @ -562,6 +562,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)", | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 David Baker
						David Baker