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, }); };