From d88b8efd199eb302df084d820cfaaca0ea6e2d72 Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Fri, 29 Oct 2021 18:11:39 -0400 Subject: [PATCH] Improve device list in Security & Privacy settings (#7004) Overhaul the device list in the "Security and Privacy" settings tab to include device trust status, provide buttons for verifying unverified devices, and improve overall usability and style. This should now be the primary interface for checking and changing the trust status of your own devices, rather than looking at your own user profile in the right panel. --- res/css/views/elements/_AccessibleButton.scss | 40 +++ res/css/views/settings/_DevicesPanel.scss | 81 ++++-- .../structures/auth/SetupEncryptionBody.tsx | 12 +- .../views/settings/DevicesPanel.tsx | 240 +++++++++++++++--- .../views/settings/DevicesPanelEntry.tsx | 180 ++++++++++--- .../tabs/user/SecurityUserSettingsTab.tsx | 16 +- src/i18n/strings/en_EN.json | 43 ++-- src/toasts/BulkUnverifiedSessionsToast.ts | 7 +- 8 files changed, 489 insertions(+), 130 deletions(-) diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 7bc47a3c98..bb006b16da 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -115,3 +115,43 @@ limitations under the License. .mx_AccessibleButton_kind_link_sm.mx_AccessibleButton_disabled { opacity: 0.4; } + +.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_confirm_sm { + background-color: $button-primary-bg-color; + + &::before { + mask-image: url('$(res)/img/feather-customised/check.svg'); + } +} + +.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_cancel_sm { + background-color: $button-danger-bg-color; + + &::before { + mask-image: url('$(res)/img/feather-customised/x.svg'); + } +} + +.mx_AccessibleButton_kind_confirm_sm, +.mx_AccessibleButton_kind_cancel_sm { + padding: 0px; + width: 16px; + height: 16px; + border-radius: 100%; + position: relative; + display: block; + + &::before { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: #ffffff; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 80%; + } +} diff --git a/res/css/views/settings/_DevicesPanel.scss b/res/css/views/settings/_DevicesPanel.scss index 7d6db7bc96..1732688cc8 100644 --- a/res/css/views/settings/_DevicesPanel.scss +++ b/res/css/views/settings/_DevicesPanel.scss @@ -15,42 +15,81 @@ limitations under the License. */ .mx_DevicesPanel { - table-layout: fixed; - // Normally the panel is 880px, however this can easily overflow the container. - // TODO: Fix the table to not be squishy width: auto; max-width: 880px; - border-spacing: 10px; + + hr { + opacity: 0.2; + border: none; + border-bottom: 1px solid $primary-content; + } } .mx_DevicesPanel_header { - font-weight: bold; + display: flex; + align-items: center; + margin-block: 10px; + + .mx_DevicesPanel_header_title { + font-size: $font-18px; + font-weight: 600; + color: $primary-content; + } + + .mx_DevicesPanel_selectButton { + padding-top: 9px; + } + + .mx_E2EIcon { + width: 24px; + height: 24px; + margin-left: 0; + margin-right: 5px; + } } -.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons { - height: 48px; // make this tall so the table doesn't move down when the delete button appears - width: 20%; +.mx_DevicesPanel_deleteButton { + margin-top: 10px; } -.mx_DevicesPanel_header th { - padding: 0px; - text-align: left; - vertical-align: middle; +.mx_DevicesPanel_device { + display: flex; + align-items: flex-start; + margin-block: 10px; + min-height: 35px; } -.mx_DevicesPanel_header .mx_DevicesPanel_deviceName { - width: 50%; +.mx_DevicesPanel_icon, .mx_DevicesPanel_checkbox { + margin-left: 9px; + margin-top: 2px; } -.mx_DevicesPanel_header .mx_DevicesPanel_deviceLastSeen { - width: 30%; +.mx_DevicesPanel_deviceInfo { + flex-grow: 1; } -.mx_DevicesPanel_device td { - vertical-align: baseline; - padding: 0px; +.mx_DevicesPanel_deviceName { + color: $primary-content; } -.mx_DevicesPanel_myDevice { - font-weight: bold; +.mx_DevicesPanel_lastSeen { + font-size: $font-12px; +} + +.mx_DevicesPanel_deviceButtons { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 9px; +} + +.mx_DevicesPanel_renameForm { + display: flex; + align-items: center; + gap: 5px; + + .mx_Field_input { + width: 240px; + margin: 0; + } } diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index e2b1aebcfd..32cd1d83ce 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -39,7 +39,7 @@ function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean { } interface IProps { - onFinished: (boolean) => void; + onFinished: () => void; } interface IState { @@ -70,7 +70,7 @@ export default class SetupEncryptionBody extends React.Component private onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); if (store.phase === Phase.Finished) { - this.props.onFinished(true); + this.props.onFinished(); return; } this.setState({ @@ -97,13 +97,16 @@ export default class SetupEncryptionBody extends React.Component const userId = cli.getUserId(); const requestPromise = cli.requestVerification(userId); - this.props.onFinished(true); + // We need to call onFinished now to close this dialog, and + // again later to signal that the verification is complete. + this.props.onFinished(); Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, { verificationRequestPromise: requestPromise, member: cli.getUser(userId), onFinished: async () => { const request = await requestPromise; request.cancel(); + this.props.onFinished(); }, }); }; @@ -125,6 +128,7 @@ export default class SetupEncryptionBody extends React.Component }; private onResetConfirmClick = () => { + this.props.onFinished(); const store = SetupEncryptionStore.sharedInstance(); store.resetConfirm(); }; @@ -140,7 +144,7 @@ export default class SetupEncryptionBody extends React.Component }; private onEncryptionPanelClose = () => { - this.props.onFinished(false); + this.props.onFinished(); }; public render() { diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index c2dc924694..957583ab9c 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -27,6 +27,7 @@ import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog"; import DevicesPanelEntry from "./DevicesPanelEntry"; import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; +import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { logger } from "matrix-js-sdk/src/logger"; @@ -36,6 +37,7 @@ interface IProps { interface IState { devices: IMyDevice[]; + crossSigningInfo?: CrossSigningInfo; deviceLoadError?: string; selectedDevices: string[]; deleting?: boolean; @@ -51,6 +53,7 @@ export default class DevicesPanel extends React.Component { devices: [], selectedDevices: [], }; + this.loadDevices = this.loadDevices.bind(this); } public componentDidMount(): void { @@ -62,20 +65,34 @@ export default class DevicesPanel extends React.Component { } private loadDevices(): void { - MatrixClientPeg.get().getDevices().then( + const cli = MatrixClientPeg.get(); + cli.getDevices().then( (resp) => { if (this.unmounted) { return; } - this.setState({ devices: resp.devices || [] }); + + const crossSigningInfo = cli.getStoredCrossSigningForUser(cli.getUserId()); + this.setState((state, props) => { + const deviceIds = resp.devices.map((device) => device.device_id); + const selectedDevices = state.selectedDevices.filter( + (deviceId) => deviceIds.includes(deviceId), + ); + return { + devices: resp.devices || [], + selectedDevices, + crossSigningInfo: crossSigningInfo, + }; + }); + console.log(this.state); }, (error) => { if (this.unmounted) { return; } let errtxt; if (error.httpStatus == 404) { // 404 probably means the HS doesn't yet support the API. - errtxt = _t("Your homeserver does not support session management."); + errtxt = _t("Your homeserver does not support device management."); } else { logger.error("Error loading sessions:", error); - errtxt = _t("Unable to load session list"); + errtxt = _t("Unable to load device list"); } this.setState({ deviceLoadError: errtxt }); }, @@ -98,6 +115,22 @@ export default class DevicesPanel extends React.Component { return (idA < idB) ? -1 : (idA > idB) ? 1 : 0; } + private isDeviceVerified(device: IMyDevice): boolean | null { + try { + const cli = MatrixClientPeg.get(); + const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id); + return this.state.crossSigningInfo.checkDeviceTrust( + this.state.crossSigningInfo, + deviceInfo, + false, + true, + ).isCrossSigningVerified(); + } catch (e) { + console.error("Error getting device cross-signing info", e); + return null; + } + } + private onDeviceSelectionToggled = (device: IMyDevice): void => { if (this.unmounted) { return; } @@ -117,7 +150,40 @@ export default class DevicesPanel extends React.Component { }); }; + private selectAll = (devices: IMyDevice[]): void => { + this.setState((state, props) => { + const selectedDevices = state.selectedDevices.slice(); + + for (const device of devices) { + const deviceId = device.device_id; + if (!selectedDevices.includes(deviceId)) { + selectedDevices.push(deviceId); + } + } + + return { selectedDevices }; + }); + }; + + private deselectAll = (devices: IMyDevice[]): void => { + this.setState((state, props) => { + const selectedDevices = state.selectedDevices.slice(); + + for (const device of devices) { + const deviceId = device.device_id; + const i = selectedDevices.indexOf(deviceId); + if (i !== -1) { + selectedDevices.splice(i, 1); + } + } + + return { selectedDevices }; + }); + }; + private onDeleteClick = (): void => { + if (this.state.selectedDevices.length === 0) { return; } + this.setState({ deleting: true, }); @@ -135,18 +201,18 @@ export default class DevicesPanel extends React.Component { const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), - body: _t("Confirm deleting these sessions by using Single Sign On to prove your identity.", { + body: _t("Confirm logging out these devices by using Single Sign On to prove your identity.", { count: numDevices, }), continueText: _t("Single Sign On"), continueKind: "primary", }, [SSOAuthEntry.PHASE_POSTAUTH]: { - title: _t("Confirm deleting these sessions"), - body: _t("Click the button below to confirm deleting these sessions.", { + title: _t("Confirm signing out these devices"), + body: _t("Click the button below to confirm signing out these devices.", { count: numDevices, }), - continueText: _t("Delete sessions", { count: numDevices }), + continueText: _t("Sign out devices", { count: numDevices }), continueKind: "danger", }, }; @@ -174,34 +240,46 @@ export default class DevicesPanel extends React.Component { private makeDeleteRequest(auth?: any): Promise { return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then( () => { - // Remove the deleted devices from `devices`, reset selection to [] + // Reset selection to [], update device list this.setState({ - devices: this.state.devices.filter( - (d) => !this.state.selectedDevices.includes(d.device_id), - ), selectedDevices: [], }); + this.loadDevices(); }, ); } private renderDevice = (device: IMyDevice): JSX.Element => { + const myDeviceId = MatrixClientPeg.get().getDeviceId(); + const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId)); + + const isOwnDevice = device.device_id === myDeviceId; + + // If our own device is unverified, it can't verify other + // devices, it can only request verification for itself + const canBeVerified = (myDevice && this.isDeviceVerified(myDevice)) || isOwnDevice; + return ; }; public render(): JSX.Element { + const loadError = ( +
+ { this.state.deviceLoadError } +
+ ); + if (this.state.deviceLoadError !== undefined) { - const classes = classNames(this.props.className, "error"); - return ( -
- { this.state.deviceLoadError } -
- ); + return loadError; } const devices = this.state.devices; @@ -210,31 +288,121 @@ export default class DevicesPanel extends React.Component { return ; } - devices.sort(this.deviceCompare); + const myDeviceId = MatrixClientPeg.get().getDeviceId(); + const myDevice = devices.find((device) => (device.device_id === myDeviceId)); + if (!myDevice) { + return loadError; + } + + const otherDevices = devices.filter((device) => (device.device_id !== myDeviceId)); + otherDevices.sort(this.deviceCompare); + + const verifiedDevices = []; + const unverifiedDevices = []; + const nonCryptoDevices = []; + for (const device of otherDevices) { + const verified = this.isDeviceVerified(device); + if (verified === true) { + verifiedDevices.push(device); + } else if (verified === false) { + unverifiedDevices.push(device); + } else { + nonCryptoDevices.push(device); + } + } + + const section = (trustIcon: JSX.Element, title: string, deviceList: IMyDevice[]): JSX.Element => { + if (deviceList.length === 0) { + return ; + } + + let selectButton: JSX.Element; + if (deviceList.length > 1) { + const anySelected = deviceList.some((device) => this.state.selectedDevices.includes(device.device_id)); + const buttonAction = anySelected ? + () => { this.deselectAll(deviceList); } : + () => { this.selectAll(deviceList); }; + const buttonText = anySelected ? _t("Deselect all") : _t("Select all"); + selectButton =
+ + { buttonText } + +
; + } + + return +
+
+
+ { trustIcon } +
+
+ { title } +
+ { selectButton } +
+ { deviceList.map(this.renderDevice) } +
; + }; + + const verifiedDevicesSection = section( + , + _t("Verified devices"), + verifiedDevices, + ); + + const unverifiedDevicesSection = section( + , + _t("Unverified devices"), + unverifiedDevices, + ); + + const nonCryptoDevicesSection = section( + , + _t("Devices without encryption support"), + nonCryptoDevices, + ); const deleteButton = this.state.deleting ? : - - { _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) } + + { _t("Sign out %(count)s selected devices", { count: this.state.selectedDevices.length }) } ; + const otherDevicesSection = (otherDevices.length > 0) ? + + { verifiedDevicesSection } + { unverifiedDevicesSection } + { nonCryptoDevicesSection } + { deleteButton } + : + +
+
+ { _t("You aren't signed into any other devices.") } +
+
; + const classes = classNames(this.props.className, "mx_DevicesPanel"); return ( - - - - - - - - - - - { devices.map(this.renderDevice) } - -
{ _t("ID") }{ _t("Public Name") }{ _t("Last seen") } - { this.state.selectedDevices.length > 0 ? deleteButton : null } -
+
+
+
+ { _t("This device") } +
+
+ { this.renderDevice(myDevice) } + { otherDevicesSection } +
); } } diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 6d73e1fe86..1af0bac425 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -22,34 +22,98 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { formatDate } from '../../../DateUtils'; import StyledCheckbox from '../elements/StyledCheckbox'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import EditableTextContainer from "../elements/EditableTextContainer"; +import AccessibleButton from "../elements/AccessibleButton"; +import Field from "../elements/Field"; +import TextWithTooltip from "../elements/TextWithTooltip"; +import Modal from "../../../Modal"; +import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; +import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; +import LogoutDialog from '../dialogs/LogoutDialog'; import { logger } from "matrix-js-sdk/src/logger"; interface IProps { - device?: IMyDevice; - onDeviceToggled?: (device: IMyDevice) => void; - selected?: boolean; + device: IMyDevice; + isOwnDevice: boolean; + verified: boolean | null; + canBeVerified: boolean; + onDeviceChange: () => void; + onDeviceToggled: (device: IMyDevice) => void; + selected: boolean; +} + +interface IState { + renaming: boolean; + displayName: string; } @replaceableComponent("views.settings.DevicesPanelEntry") -export default class DevicesPanelEntry extends React.Component { - public static defaultProps = { - onDeviceToggled: () => {}, +export default class DevicesPanelEntry extends React.Component { + constructor(props: IProps) { + super(props); + this.state = { + renaming: false, + displayName: props.device.display_name, + }; + } + + private onDeviceToggled = (): void => { + this.props.onDeviceToggled(this.props.device); }; - private onDisplayNameChanged = (value: string): Promise<{}> => { - const device = this.props.device; - return MatrixClientPeg.get().setDeviceDetails(device.device_id, { - display_name: value, + private onRename = (): void => { + this.setState({ renaming: true }); + }; + + private onChangeDisplayName = (ev: React.ChangeEvent): void => { + this.setState({ + displayName: ev.target.value, + }); + }; + + private onRenameSubmit = async () => { + this.setState({ renaming: false }); + await MatrixClientPeg.get().setDeviceDetails(this.props.device.device_id, { + display_name: this.state.displayName, }).catch((e) => { logger.error("Error setting session display name", e); throw new Error(_t("Failed to set display name")); }); + this.props.onDeviceChange(); }; - private onDeviceToggled = (): void => { - this.props.onDeviceToggled(this.props.device); + private onRenameCancel = (): void => { + this.setState({ renaming: false }); + }; + + private onOwnDeviceSignOut = (): void => { + Modal.createTrackedDialog('Logout from device list', '', LogoutDialog, + /* props= */{}, /* className= */null, + /* isPriority= */false, /* isStatic= */true); + }; + + private verify = async () => { + if (this.props.isOwnDevice) { + Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog, { + onFinished: this.props.onDeviceChange, + }); + } else { + const cli = MatrixClientPeg.get(); + const userId = cli.getUserId(); + const verificationRequestPromise = cli.requestVerification( + userId, + [this.props.device.device_id], + ); + Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, { + verificationRequestPromise, + member: cli.getUser(userId), + onFinished: async () => { + const request = await verificationRequestPromise; + request.cancel(); + this.props.onDeviceChange(); + }, + }); + } }; public render(): JSX.Element { @@ -57,34 +121,78 @@ export default class DevicesPanelEntry extends React.Component { let lastSeen = ""; if (device.last_seen_ts) { - const lastSeenDate = formatDate(new Date(device.last_seen_ts)); - lastSeen = device.last_seen_ip + " @ " + - lastSeenDate.toLocaleString(); + const lastSeenDate = new Date(device.last_seen_ts); + lastSeen = _t("Last seen %(date)s at %(ip)s", { + date: formatDate(lastSeenDate), + ip: device.last_seen_ip, + }); } - let myDeviceClass = ''; - if (device.device_id === MatrixClientPeg.get().getDeviceId()) { - myDeviceClass = " mx_DevicesPanel_myDevice"; + const myDeviceClass = this.props.isOwnDevice ? " mx_DevicesPanel_myDevice" : ''; + + let iconClass = ''; + let verifyButton: JSX.Element; + if (this.props.verified !== null) { + iconClass = this.props.verified ? "mx_E2EIcon_verified" : "mx_E2EIcon_warning"; + if (!this.props.verified && this.props.canBeVerified) { + verifyButton = + { _t("Verify") } + ; + } } + let signOutButton: JSX.Element; + if (this.props.isOwnDevice) { + signOutButton = + { _t("Sign Out") } + ; + } + + const left = this.props.isOwnDevice ? +
+ +
: +
+ +
; + + const buttons = this.state.renaming ? +
+ + + + : + + { signOutButton } + { verifyButton } + + { _t("Rename") } + + ; + return ( - - - { device.device_id } - - - - - - { lastSeen } - - - - - +
+ { left } +
+
+ + { device.display_name } + +
+
+ { lastSeen } +
+
+
+ { buttons } +
+
); } } diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index b9753d9c86..d2ab697e8f 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -326,23 +326,15 @@ export default class SecurityUserSettingsTab extends React.Component { warning } -
{ _t("Where you’re logged in") }
+
{ _t("Where you’re signed in") }
{ _t( - "Manage the names of and sign out of your sessions below or " + - "verify them in your User Profile.", {}, - { - a: sub => - { sub } - , - }, + "Manage your signed-in devices below. " + + "A device's name is visible to people you communicate with.", ) } -
- { _t("A session's public name is visible to people you communicate with") } - -
+
{ _t("Encryption") }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 178d3214fb..a76796b93f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1138,22 +1138,30 @@ "Cryptography": "Cryptography", "Session ID:": "Session ID:", "Session key:": "Session key:", - "Your homeserver does not support session management.": "Your homeserver does not support session management.", - "Unable to load session list": "Unable to load session list", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Confirm deleting these sessions by using Single Sign On to prove your identity.", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Confirm deleting this session by using Single Sign On to prove your identity.", - "Confirm deleting these sessions": "Confirm deleting these sessions", - "Click the button below to confirm deleting these sessions.|other": "Click the button below to confirm deleting these sessions.", - "Click the button below to confirm deleting these sessions.|one": "Click the button below to confirm deleting this session.", - "Delete sessions|other": "Delete sessions", - "Delete sessions|one": "Delete session", + "Your homeserver does not support device management.": "Your homeserver does not support device management.", + "Unable to load device list": "Unable to load device list", + "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", + "Confirm signing out these devices": "Confirm signing out these devices", + "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", + "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", + "Sign out devices|other": "Sign out devices", + "Sign out devices|one": "Sign out device", "Authentication": "Authentication", - "Delete %(count)s sessions|other": "Delete %(count)s sessions", - "Delete %(count)s sessions|one": "Delete %(count)s session", - "ID": "ID", - "Public Name": "Public Name", - "Last seen": "Last seen", + "Deselect all": "Deselect all", + "Select all": "Select all", + "Verified devices": "Verified devices", + "Unverified devices": "Unverified devices", + "Devices without encryption support": "Devices without encryption support", + "Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices", + "Sign out %(count)s selected devices|one": "Sign out %(count)s selected device", + "You aren't signed into any other devices.": "You aren't signed into any other devices.", + "This device": "This device", "Failed to set display name": "Failed to set display name", + "Last seen %(date)s at %(ip)s": "Last seen %(date)s at %(ip)s", + "Sign Out": "Sign Out", + "Display Name": "Display Name", + "Rename": "Rename", "Encryption": "Encryption", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", @@ -1217,7 +1225,6 @@ "The operation could not be completed": "The operation could not be completed", "Upgrade to your own domain": "Upgrade to your own domain", "Profile": "Profile", - "Display Name": "Display Name", "Profile picture": "Profile picture", "Save": "Save", "Delete Backup": "Delete Backup", @@ -1416,9 +1423,8 @@ "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s collects anonymous analytics to allow us to improve the application.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", "Learn more about how we use analytics.": "Learn more about how we use analytics.", - "Where you’re logged in": "Where you’re logged in", - "Manage the names of and sign out of your sessions below or verify them in your User Profile.": "Manage the names of and sign out of your sessions below or verify them in your User Profile.", - "A session's public name is visible to people you communicate with": "A session's public name is visible to people you communicate with", + "Where you’re signed in": "Where you’re signed in", + "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", "Default Device": "Default Device", "No media permissions": "No media permissions", "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", @@ -1859,6 +1865,7 @@ "Room settings": "Room settings", "Trusted": "Trusted", "Not trusted": "Not trusted", + "Unable to load session list": "Unable to load session list", "%(count)s verified sessions|other": "%(count)s verified sessions", "%(count)s verified sessions|one": "1 verified session", "Hide verified sessions": "Hide verified sessions", diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index e063f72fe0..0a35a91345 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -16,10 +16,11 @@ 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"; +import { Action } from "../dispatcher/actions"; +import { UserTab } from "../components/views/dialogs/UserSettingsDialog"; const TOAST_KEY = "reviewsessions"; @@ -28,8 +29,8 @@ export const showToast = (deviceIds: Set) => { DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds); dis.dispatch({ - action: 'view_user_info', - userId: MatrixClientPeg.get().getUserId(), + action: Action.ViewUserSettings, + initialTabId: UserTab.Security, }); };