From 530197bfcd5c13ea312a854f1d6bc260d07fde0f Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 26 May 2023 13:58:28 +1200 Subject: [PATCH] Move session manager out of beta (#10968) * remove old device manager * undo type fix for cypress crypto * update test case --- cypress/e2e/register/register.spec.ts | 10 +- .../e2e/settings/device-management.spec.ts | 1 - res/css/_components.pcss | 1 - res/css/views/settings/_DevicesPanel.pcss | 102 ---- .../handlers/viewUserDeviceSettings.ts | 7 +- src/components/structures/MatrixChat.tsx | 2 +- .../views/dialogs/UserSettingsDialog.tsx | 34 +- .../views/settings/DevicesPanel.tsx | 365 ------------- .../views/settings/DevicesPanelEntry.tsx | 195 ------- .../tabs/user/SecurityUserSettingsTab.tsx | 51 -- src/i18n/strings/en_EN.json | 26 +- src/settings/Settings.tsx | 21 - .../handlers/viewUserDeviceSettings-test.ts | 15 +- .../components/structures/MatrixChat-test.tsx | 2 +- .../views/dialogs/UserSettingsDialog-test.tsx | 18 +- .../UserSettingsDialog-test.tsx.snap | 18 + .../views/settings/DevicesPanel-test.tsx | 246 --------- .../__snapshots__/DevicesPanel-test.tsx.snap | 506 ------------------ .../user/SecurityUserSettingsTab-test.tsx | 51 +- .../LabsUserSettingsTab-test.tsx.snap | 53 -- .../SecurityUserSettingsTab-test.tsx.snap | 399 ++++++++++++++ 21 files changed, 450 insertions(+), 1673 deletions(-) delete mode 100644 res/css/views/settings/_DevicesPanel.pcss delete mode 100644 src/components/views/settings/DevicesPanel.tsx delete mode 100644 src/components/views/settings/DevicesPanelEntry.tsx delete mode 100644 test/components/views/settings/DevicesPanel-test.tsx delete mode 100644 test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap create mode 100644 test/components/views/settings/tabs/user/__snapshots__/SecurityUserSettingsTab-test.tsx.snap diff --git a/cypress/e2e/register/register.spec.ts b/cypress/e2e/register/register.spec.ts index e975ad4f7d..5810915439 100644 --- a/cypress/e2e/register/register.spec.ts +++ b/cypress/e2e/register/register.spec.ts @@ -89,11 +89,11 @@ describe("Registration", () => { // check that the device considers itself verified cy.findByRole("button", { name: "User menu" }).click(); - cy.findByRole("menuitem", { name: "Security & Privacy" }).click(); - cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon").should( - "have.class", - "mx_E2EIcon_verified", - ); + cy.findByRole("menuitem", { name: "All settings" }).click(); + cy.findByRole("tab", { name: "Sessions" }).click(); + cy.findByTestId("current-session-section").within(() => { + cy.findByTestId("device-metadata-isVerified").should("have.text", "Verified"); + }); // check that cross-signing keys have been uploaded. checkDeviceIsCrossSigned(); diff --git a/cypress/e2e/settings/device-management.spec.ts b/cypress/e2e/settings/device-management.spec.ts index 277fa505fc..06795b68be 100644 --- a/cypress/e2e/settings/device-management.spec.ts +++ b/cypress/e2e/settings/device-management.spec.ts @@ -24,7 +24,6 @@ describe("Device manager", () => { let user: UserCredentials | undefined; beforeEach(() => { - cy.enableLabsFeature("feature_new_device_manager"); cy.startHomeserver("default").then((data) => { homeserver = data; diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 4765904241..56628095f2 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -316,7 +316,6 @@ @import "./views/settings/_AvatarSetting.pcss"; @import "./views/settings/_CrossSigningPanel.pcss"; @import "./views/settings/_CryptographyPanel.pcss"; -@import "./views/settings/_DevicesPanel.pcss"; @import "./views/settings/_FontScalingPanel.pcss"; @import "./views/settings/_ImageSizePanel.pcss"; @import "./views/settings/_IntegrationManager.pcss"; diff --git a/res/css/views/settings/_DevicesPanel.pcss b/res/css/views/settings/_DevicesPanel.pcss deleted file mode 100644 index 69b0d0a664..0000000000 --- a/res/css/views/settings/_DevicesPanel.pcss +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_DevicesPanel { - width: auto; - max-width: 880px; - - hr { - border: none; - border-bottom: 1px solid $quinary-content; - } -} - -.mx_DevicesPanel_header { - display: flex; - align-items: center; - margin-block: 10px; - - .mx_DevicesPanel_header_title { - font-size: $font-18px; - font-weight: var(--font-semi-bold); - color: $primary-content; - } - - .mx_DevicesPanel_selectButton { - padding-top: 9px; - } - - .mx_E2EIcon { - width: 24px; - height: 24px; - margin-left: 0; - margin-right: 5px; - } -} - -.mx_DevicesPanel_deleteButton { - margin-top: 10px; -} - -.mx_DevicesPanel_device { - display: flex; - align-items: flex-start; - margin-block: 10px; - min-height: 35px; - padding: 0 $spacing-8; - - .mx_DeviceTypeIcon { - /* hide the new device type in legacy device list - for backwards compat reasons */ - display: none; - } -} - -.mx_DevicesPanel_icon { - margin-left: 0px; - margin-right: $spacing-16; - margin-top: 2px; -} - -.mx_DevicesPanel_deviceInfo { - flex-grow: 1; -} - -.mx_DevicesPanel_deviceName { - color: $primary-content; -} - -.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/actions/handlers/viewUserDeviceSettings.ts b/src/actions/handlers/viewUserDeviceSettings.ts index 4525ba104d..8af795e51f 100644 --- a/src/actions/handlers/viewUserDeviceSettings.ts +++ b/src/actions/handlers/viewUserDeviceSettings.ts @@ -19,12 +19,11 @@ import { Action } from "../../dispatcher/actions"; import defaultDispatcher from "../../dispatcher/dispatcher"; /** - * Redirect to the correct device manager section - * Based on the labs setting + * Open user device manager settings */ -export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean): void => { +export const viewUserDeviceSettings = (): void => { defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: isNewDeviceManagerEnabled ? UserTab.SessionManager : UserTab.Security, + initialTabId: UserTab.SessionManager, }); }; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a9f5c54279..6798d6a444 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -700,7 +700,7 @@ export default class MatrixChat extends React.PureComponent { break; } case Action.ViewUserDeviceSettings: { - viewUserDeviceSettings(SettingsStore.getValue("feature_new_device_manager")); + viewUserDeviceSettings(); break; } case Action.ViewUserSettings: { diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 5f534bba1f..c185da1f2d 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -45,7 +45,6 @@ interface IProps { interface IState { mjolnirEnabled: boolean; - newSessionManagerEnabled: boolean; } export default class UserSettingsDialog extends React.Component { @@ -56,15 +55,11 @@ export default class UserSettingsDialog extends React.Component this.state = { mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), - newSessionManagerEnabled: SettingsStore.getValue("feature_new_device_manager"), }; } public componentDidMount(): void { - this.settingsWatchers = [ - SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged), - SettingsStore.watchSetting("feature_new_device_manager", null, this.sessionManagerChanged), - ]; + this.settingsWatchers = [SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged)]; } public componentWillUnmount(): void { @@ -76,11 +71,6 @@ export default class UserSettingsDialog extends React.Component this.setState({ mjolnirEnabled: newValue }); }; - private sessionManagerChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => { - // We can cheat because we know what levels a feature is tracked at, and how it is tracked - this.setState({ newSessionManagerEnabled: newValue }); - }; - private getTabs(): NonEmptyArray> { const tabs: Tab[] = []; @@ -160,18 +150,16 @@ export default class UserSettingsDialog extends React.Component "UserSettingsSecurityPrivacy", ), ); - if (this.state.newSessionManagerEnabled) { - tabs.push( - new Tab( - UserTab.SessionManager, - _td("Sessions"), - "mx_UserSettingsDialog_sessionsIcon", - , - // don't track with posthog while under construction - undefined, - ), - ); - } + tabs.push( + new Tab( + UserTab.SessionManager, + _td("Sessions"), + "mx_UserSettingsDialog_sessionsIcon", + , + // don't track with posthog while under construction + undefined, + ), + ); // Show the Labs tab if enabled or if there are any active betas if ( SdkConfig.get("show_labs_settings") || diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx deleted file mode 100644 index 06bdc5fea9..0000000000 --- a/src/components/views/settings/DevicesPanel.tsx +++ /dev/null @@ -1,365 +0,0 @@ -/* -Copyright 2016 - 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import classNames from "classnames"; -import { IMyDevice } from "matrix-js-sdk/src/client"; -import { logger } from "matrix-js-sdk/src/logger"; -import { CryptoEvent } from "matrix-js-sdk/src/crypto"; - -import { _t } from "../../../languageHandler"; -import DevicesPanelEntry from "./DevicesPanelEntry"; -import Spinner from "../elements/Spinner"; -import AccessibleButton from "../elements/AccessibleButton"; -import { deleteDevicesWithInteractiveAuth } from "./devices/deleteDevices"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { fetchExtendedDeviceInformation } from "./devices/useOwnDevices"; -import { DevicesDictionary, ExtendedDevice } from "./devices/types"; - -interface IProps { - className?: string; -} - -interface IState { - devices?: DevicesDictionary; - deviceLoadError?: string; - selectedDevices: string[]; - deleting?: boolean; -} - -export default class DevicesPanel extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; - private unmounted = false; - - public constructor(props: IProps) { - super(props); - this.state = { - selectedDevices: [], - }; - this.loadDevices = this.loadDevices.bind(this); - } - - public componentDidMount(): void { - this.context.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); - this.loadDevices(); - } - - public componentWillUnmount(): void { - this.context.off(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); - this.unmounted = true; - } - - private onDevicesUpdated = (users: string[]): void => { - if (!users.includes(this.context.getUserId()!)) return; - this.loadDevices(); - }; - - private loadDevices(): void { - const cli = this.context; - fetchExtendedDeviceInformation(cli).then( - (devices) => { - if (this.unmounted) { - return; - } - - this.setState((state, props) => { - return { - devices: devices, - selectedDevices: state.selectedDevices.filter((deviceId) => devices.hasOwnProperty(deviceId)), - }; - }); - }, - (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 device management."); - } else { - logger.error("Error loading sessions:", error); - errtxt = _t("Unable to load device list"); - } - this.setState({ deviceLoadError: errtxt }); - }, - ); - } - - /* - * compare two devices, sorting from most-recently-seen to least-recently-seen - * (and then, for stability, by device id) - */ - private deviceCompare(a: IMyDevice, b: IMyDevice): number { - // return < 0 if a comes before b, > 0 if a comes after b. - const lastSeenDelta = (b.last_seen_ts || 0) - (a.last_seen_ts || 0); - - if (lastSeenDelta !== 0) { - return lastSeenDelta; - } - - const idA = a.device_id; - const idB = b.device_id; - return idA < idB ? -1 : idA > idB ? 1 : 0; - } - - private onDeviceSelectionToggled = (device: IMyDevice): void => { - if (this.unmounted) { - return; - } - - const deviceId = device.device_id; - this.setState((state, props) => { - // Make a copy of the selected devices, then add or remove the device - const selectedDevices = state.selectedDevices.slice(); - - const i = selectedDevices.indexOf(deviceId); - if (i === -1) { - selectedDevices.push(deviceId); - } else { - selectedDevices.splice(i, 1); - } - - return { selectedDevices }; - }); - }; - - 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 = async (): Promise => { - if (this.state.selectedDevices.length === 0) { - return; - } - - this.setState({ - deleting: true, - }); - - try { - await deleteDevicesWithInteractiveAuth(this.context, this.state.selectedDevices, (success) => { - if (success) { - // Reset selection to [], update device list - this.setState({ - selectedDevices: [], - }); - this.loadDevices(); - } - this.setState({ - deleting: false, - }); - }); - } catch (error) { - logger.error("Error deleting sessions", error); - this.setState({ - deleting: false, - }); - } - }; - - private renderDevice = (device: ExtendedDevice): JSX.Element => { - const myDeviceId = this.context.getDeviceId()!; - const myDevice = this.state.devices?.[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 && myDevice.isVerified) || isOwnDevice; - - return ( - - ); - }; - - public render(): React.ReactNode { - const loadError =
{this.state.deviceLoadError}
; - - if (this.state.deviceLoadError !== undefined) { - return loadError; - } - - const devices = this.state.devices; - if (devices === undefined) { - // still loading - return ; - } - - const myDeviceId = this.context.getDeviceId()!; - const myDevice = devices[myDeviceId]; - - if (!myDevice) { - return loadError; - } - - const otherDevices = Object.values(devices).filter((device) => device.device_id !== myDeviceId); - otherDevices.sort(this.deviceCompare); - - const verifiedDevices: ExtendedDevice[] = []; - const unverifiedDevices: ExtendedDevice[] = []; - const nonCryptoDevices: ExtendedDevice[] = []; - for (const device of otherDevices) { - const verified = device.isVerified; - 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: ExtendedDevice[]): JSX.Element => { - if (deviceList.length === 0) { - return ; - } - - let selectButton: JSX.Element | undefined; - 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("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 ( -
-
-
{_t("This device")}
-
- {this.renderDevice(myDevice)} - {otherDevicesSection} -
- ); - } -} diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx deleted file mode 100644 index 95bcdb9e94..0000000000 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/* -Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { IMyDevice } from "matrix-js-sdk/src/client"; -import { logger } from "matrix-js-sdk/src/logger"; -import classNames from "classnames"; - -import { _t } from "../../../languageHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import AccessibleButton from "../elements/AccessibleButton"; -import Field from "../elements/Field"; -import Modal from "../../../Modal"; -import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog"; -import VerificationRequestDialog from "../../views/dialogs/VerificationRequestDialog"; -import LogoutDialog from "../dialogs/LogoutDialog"; -import DeviceTile from "./devices/DeviceTile"; -import SelectableDeviceTile from "./devices/SelectableDeviceTile"; -import { DeviceType } from "../../../utils/device/parseUserAgent"; - -interface IProps { - device: IMyDevice; - isOwnDevice: boolean; - verified: boolean | null; - canBeVerified: boolean; - onDeviceChange: () => void; - onDeviceToggled: (device: IMyDevice) => void; - selected: boolean; -} - -interface IState { - renaming: boolean; - displayName: string; -} - -export default class DevicesPanelEntry extends React.Component { - public constructor(props: IProps) { - super(props); - this.state = { - renaming: false, - displayName: props.device.display_name ?? "", - }; - } - - private onDeviceToggled = (): void => { - this.props.onDeviceToggled(this.props.device); - }; - - private onRename = (): void => { - this.setState({ renaming: true }); - }; - - private onChangeDisplayName = (ev: React.ChangeEvent): void => { - this.setState({ - displayName: ev.target.value, - }); - }; - - private onRenameSubmit = async (): Promise => { - 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 onRenameCancel = (): void => { - this.setState({ renaming: false }); - }; - - private onOwnDeviceSignOut = (): void => { - Modal.createDialog( - LogoutDialog, - /* props= */ {}, - /* className= */ undefined, - /* isPriority= */ false, - /* isStatic= */ true, - ); - }; - - private verify = async (): Promise => { - if (this.props.isOwnDevice) { - Modal.createDialog(SetupEncryptionDialog, { - onFinished: this.props.onDeviceChange, - }); - } else { - const cli = MatrixClientPeg.get(); - const userId = cli.getSafeUserId(); - const verificationRequestPromise = cli.requestVerification(userId, [this.props.device.device_id]); - Modal.createDialog(VerificationRequestDialog, { - verificationRequestPromise, - member: cli.getUser(userId) ?? undefined, - onFinished: async (): Promise => { - const request = await verificationRequestPromise; - request.cancel(); - this.props.onDeviceChange(); - }, - }); - } - }; - - public render(): React.ReactNode { - let iconClass = ""; - let verifyButton: JSX.Element | undefined; - 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 | undefined; - if (this.props.isOwnDevice) { - signOutButton = ( - - {_t("Sign Out")} - - ); - } - - const buttons = this.state.renaming ? ( -
- - - - - ) : ( - - {signOutButton} - {verifyButton} - - {_t("Rename")} - - - ); - - const extendedDevice = { - ...this.props.device, - isVerified: this.props.verified, - deviceType: DeviceType.Unknown, - }; - - if (this.props.isOwnDevice) { - return ( -
-
- -
- {buttons} -
- ); - } - - return ( -
- - {buttons} - -
- ); - } -} diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index e72b045ae9..c902577b61 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -30,7 +30,6 @@ import { UIFeature } from "../../../../../settings/UIFeature"; import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; import { ActionPayload } from "../../../../../dispatcher/payloads"; import CryptographyPanel from "../../CryptographyPanel"; -import DevicesPanel from "../../DevicesPanel"; import SettingsFlag from "../../../elements/SettingsFlag"; import CrossSigningPanel from "../../CrossSigningPanel"; import EventIndexPanel from "../../EventIndexPanel"; @@ -38,8 +37,6 @@ import InlineSpinner from "../../../elements/InlineSpinner"; import { PosthogAnalytics } from "../../../../../PosthogAnalytics"; import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog"; import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; -import LoginWithQR, { Mode } from "../../../auth/LoginWithQR"; -import LoginWithQRSection from "../../devices/LoginWithQRSection"; import type { IServerVersions } from "matrix-js-sdk/src/matrix"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; @@ -83,10 +80,7 @@ interface IState { waitingUnignored: string[]; managingInvites: boolean; invitedRoomIds: Set; - showLoginWithQR: Mode | null; versions?: IServerVersions; - // we can't use the capabilities type from the js-sdk because it isn't exported - capabilities?: Record; } export default class SecurityUserSettingsTab extends React.Component { @@ -103,7 +97,6 @@ export default class SecurityUserSettingsTab extends React.Component this.setState({ versions })); - MatrixClientPeg.get() - .getCapabilities() - .then((capabilities) => this.setState({ capabilities })); } public componentWillUnmount(): void { @@ -284,14 +274,6 @@ export default class SecurityUserSettingsTab extends React.Component { - this.setState({ showLoginWithQR: Mode.Show }); - }; - - private onLoginWithQRFinished = (): void => { - this.setState({ showLoginWithQR: null }); - }; - public render(): React.ReactNode { const secureBackup = ( @@ -374,42 +356,9 @@ export default class SecurityUserSettingsTab extends React.Component - - {_t( - "Manage your signed-in devices below. " + - "A device's name is visible to people you communicate with.", - )} - - - - - ); - - const client = MatrixClientPeg.get(); - - if (this.state.showLoginWithQR) { - return ( - - - - ); - } - return ( {warning} - {devicesSection} {secureBackup} {eventIndex} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f846205a52..ff653aa3c6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -987,10 +987,6 @@ "Favourite Messages": "Favourite Messages", "Under active development.": "Under active development.", "Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length", - "Use new session manager": "Use new session manager", - "New session manager": "New session manager", - "Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.", - "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.", "Rust cryptography implementation": "Rust cryptography implementation", "Font size": "Font size", "Use custom size": "Use custom size", @@ -1382,21 +1378,6 @@ "Cryptography": "Cryptography", "Session ID:": "Session ID:", "Session key:": "Session key:", - "Your homeserver does not support device management.": "Your homeserver does not support device management.", - "Unable to load device list": "Unable to load device list", - "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", - "Sign Out": "Sign Out", - "Display Name": "Display Name", - "Rename": "Rename", "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.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", @@ -1463,6 +1444,7 @@ "There was an error loading your notification settings.": "There was an error loading your notification settings.", "Failed to save your profile": "Failed to save your profile", "The operation could not be completed": "The operation could not be completed", + "Display Name": "Display Name", "Profile picture": "Profile picture", "Save": "Save", "Delete Backup": "Delete Backup", @@ -1660,8 +1642,6 @@ "Privacy": "Privacy", "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", "Sessions": "Sessions", - "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.", "Sign out": "Sign out", "Are you sure you want to sign out of %(count)s sessions?|other": "Are you sure you want to sign out of %(count)s sessions?", "Are you sure you want to sign out of %(count)s sessions?|one": "Are you sure you want to sign out of %(count)s session?", @@ -1814,11 +1794,13 @@ "Sign out devices|other": "Sign out devices", "Sign out devices|one": "Sign out device", "Authentication": "Authentication", + "Failed to set display name": "Failed to set display name", "Rename session": "Rename session", "Please be aware that session names are also visible to people you communicate with.": "Please be aware that session names are also visible to people you communicate with.", "Renaming sessions": "Renaming sessions", "Other users in direct messages and rooms that you join are able to view a full list of your sessions.": "Other users in direct messages and rooms that you join are able to view a full list of your sessions.", "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.": "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.", + "Rename": "Rename", "Session ID": "Session ID", "Last activity": "Last activity", "Application": "Application", @@ -1877,6 +1859,8 @@ "Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer", "Filter devices": "Filter devices", "Show": "Show", + "Deselect all": "Deselect all", + "Select all": "Select all", "%(count)s sessions selected|other": "%(count)s sessions selected", "%(count)s sessions selected|one": "%(count)s session selected", "Sign in with QR code": "Sign in with QR code", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 41774bb63c..90cfe7e338 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -445,27 +445,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { displayName: _td("Force 15s voice broadcast chunk length"), default: false, }, - "feature_new_device_manager": { - isFeature: true, - labsGroup: LabGroup.Experimental, - supportedLevels: LEVELS_FEATURE, - displayName: _td("Use new session manager"), - default: false, - betaInfo: { - title: _td("New session manager"), - caption: () => ( - <> -

{_t("Have greater visibility and control over all your sessions.")}

-

- {_t( - "Our new sessions manager provides better visibility of all your sessions, " + - "and greater control over them including the ability to remotely toggle push notifications.", - )} -

- - ), - }, - }, "feature_rust_crypto": { // use the rust matrix-sdk-crypto-js for crypto. isFeature: true, diff --git a/test/actions/handlers/viewUserDeviceSettings-test.ts b/test/actions/handlers/viewUserDeviceSettings-test.ts index 41fcc60fdf..d3cdab5fc7 100644 --- a/test/actions/handlers/viewUserDeviceSettings-test.ts +++ b/test/actions/handlers/viewUserDeviceSettings-test.ts @@ -26,23 +26,12 @@ describe("viewUserDeviceSettings()", () => { dispatchSpy.mockClear(); }); - it("dispatches action to view new session manager when enabled", () => { - const isNewDeviceManagerEnabled = true; - viewUserDeviceSettings(isNewDeviceManagerEnabled); + it("dispatches action to view session manager", () => { + viewUserDeviceSettings(); expect(dispatchSpy).toHaveBeenCalledWith({ action: Action.ViewUserSettings, initialTabId: UserTab.SessionManager, }); }); - - it("dispatches action to view old session manager when disabled", () => { - const isNewDeviceManagerEnabled = false; - viewUserDeviceSettings(isNewDeviceManagerEnabled); - - expect(dispatchSpy).toHaveBeenCalledWith({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, - }); - }); }); diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index 60a4a7b7e6..ba43911680 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -173,7 +173,7 @@ describe("", () => { expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + initialTabId: UserTab.SessionManager, }); }); }); diff --git a/test/components/views/dialogs/UserSettingsDialog-test.tsx b/test/components/views/dialogs/UserSettingsDialog-test.tsx index 5e610b87ba..bb28dae58b 100644 --- a/test/components/views/dialogs/UserSettingsDialog-test.tsx +++ b/test/components/views/dialogs/UserSettingsDialog-test.tsx @@ -117,10 +117,7 @@ describe("", () => { expect(getByTestId(`settings-tab-${UserTab.Voice}`)).toBeTruthy(); }); - it("renders session manager tab when enabled", () => { - mockSettingsStore.getValue.mockImplementation((settingName): any => { - return settingName === "feature_new_device_manager"; - }); + it("renders session manager tab", () => { const { getByTestId } = render(getComponent()); expect(getByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy(); }); @@ -153,28 +150,15 @@ describe("", () => { expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeFalsy(); expect(mockSettingsStore.watchSetting.mock.calls[0][0]).toEqual("feature_mjolnir"); - expect(mockSettingsStore.watchSetting.mock.calls[1][0]).toEqual("feature_new_device_manager"); // call the watch setting callback watchSettingCallbacks["feature_mjolnir"]("feature_mjolnir", "", SettingLevel.ACCOUNT, true, true); // tab is rendered now expect(queryByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeTruthy(); - // call the watch setting callback - watchSettingCallbacks["feature_new_device_manager"]( - "feature_new_device_manager", - "", - SettingLevel.ACCOUNT, - true, - true, - ); - // tab is rendered now - expect(queryByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy(); - unmount(); // unwatches settings on unmount expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_mjolnir"); - expect(mockSettingsStore.unwatchSetting).toHaveBeenCalledWith("mock-watcher-id-feature_new_device_manager"); }); }); diff --git a/test/components/views/dialogs/__snapshots__/UserSettingsDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/UserSettingsDialog-test.tsx.snap index 22d10366f2..3ec2cec8e1 100644 --- a/test/components/views/dialogs/__snapshots__/UserSettingsDialog-test.tsx.snap +++ b/test/components/views/dialogs/__snapshots__/UserSettingsDialog-test.tsx.snap @@ -128,6 +128,24 @@ NodeList [ Security & Privacy
, + ,
  • ", () => { - const userId = "@alice:server.org"; - - // the local device - const ownDevice = { device_id: "device_1" }; - - // a device which we have verified via cross-signing - const verifiedDevice = { device_id: "device_2" }; - - // a device which we have *not* verified via cross-signing - const unverifiedDevice = { device_id: "device_3" }; - - // a device which is returned by `getDevices` but getDeviceVerificationStatus returns `null` for - // (as it would for a device with no E2E keys). - const nonCryptoDevice = { device_id: "non_crypto" }; - - const mockCrypto = { - getDeviceVerificationStatus: jest.fn().mockImplementation((_userId, deviceId) => { - if (_userId !== userId) { - throw new Error(`bad user id ${_userId}`); - } - if (deviceId === ownDevice.device_id || deviceId === verifiedDevice.device_id) { - return { crossSigningVerified: true }; - } else if (deviceId === unverifiedDevice.device_id) { - return { - crossSigningVerified: false, - }; - } else { - return null; - } - }), - }; - const mockClient = getMockClientWithEventEmitter({ - ...mockClientMethodsUser(userId), - getDevices: jest.fn(), - getDeviceId: jest.fn().mockReturnValue(ownDevice.device_id), - deleteMultipleDevices: jest.fn(), - getStoredDevice: jest.fn().mockReturnValue(new DeviceInfo("id")), - generateClientSecret: jest.fn(), - getPushers: jest.fn(), - setPusher: jest.fn(), - getCrypto: jest.fn().mockReturnValue(mockCrypto), - }); - - const getComponent = () => ( - - - - ); - - beforeEach(() => { - jest.clearAllMocks(); - - mockClient.getDevices - .mockReset() - .mockResolvedValue({ devices: [ownDevice, verifiedDevice, unverifiedDevice, nonCryptoDevice] }); - - mockClient.getPushers.mockReset().mockResolvedValue({ - pushers: [ - mkPusher({ - [PUSHER_DEVICE_ID.name]: ownDevice.device_id, - [PUSHER_ENABLED.name]: true, - }), - ], - }); - }); - - it("renders device panel with devices", async () => { - const { container } = render(getComponent()); - await flushPromises(); - expect(container).toMatchSnapshot(); - }); - - describe("device deletion", () => { - const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } }; - - const toggleDeviceSelection = (container: HTMLElement, deviceId: string) => - act(() => { - const checkbox = container.querySelector(`#device-tile-checkbox-${deviceId}`)!; - fireEvent.click(checkbox); - }); - - beforeEach(() => { - mockClient.deleteMultipleDevices.mockReset(); - }); - - it("deletes selected devices when interactive auth is not required", async () => { - mockClient.deleteMultipleDevices.mockResolvedValue({}); - mockClient.getDevices - .mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] }) - // pretend it was really deleted on refresh - .mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] }); - - const { container, getByTestId } = render(getComponent()); - await flushPromises(); - - expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(3); - - toggleDeviceSelection(container, verifiedDevice.device_id); - - mockClient.getDevices.mockClear(); - - act(() => { - fireEvent.click(getByTestId("sign-out-devices-btn")); - }); - - expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy(); - expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], undefined); - - await flushPromises(); - - // devices refreshed - expect(mockClient.getDevices).toHaveBeenCalled(); - // and rerendered - expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(2); - }); - - it("deletes selected devices when interactive auth is required", async () => { - mockClient.deleteMultipleDevices - // require auth - .mockRejectedValueOnce(interactiveAuthError) - // then succeed - .mockResolvedValueOnce({}); - - mockClient.getDevices - .mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] }) - // pretend it was really deleted on refresh - .mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] }); - - const { container, getByTestId, getByLabelText } = render(getComponent()); - - await flushPromises(); - - // reset mock count after initial load - mockClient.getDevices.mockClear(); - - toggleDeviceSelection(container, verifiedDevice.device_id); - - act(() => { - fireEvent.click(getByTestId("sign-out-devices-btn")); - }); - - await flushPromises(); - // modal rendering has some weird sleeps - await sleep(100); - - expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], undefined); - - const modal = document.getElementsByClassName("mx_Dialog"); - expect(modal).toMatchSnapshot(); - - // fill password and submit for interactive auth - act(() => { - fireEvent.change(getByLabelText("Password"), { target: { value: "topsecret" } }); - fireEvent.submit(getByLabelText("Password")); - }); - - await flushPromises(); - - // called again with auth - expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([verifiedDevice.device_id], { - identifier: { - type: "m.id.user", - user: userId, - }, - password: "", - type: "m.login.password", - user: userId, - }); - // devices refreshed - expect(mockClient.getDevices).toHaveBeenCalled(); - // and rerendered - expect(container.getElementsByClassName("mx_DevicesPanel_device").length).toEqual(2); - }); - - it("clears loading state when interactive auth fail is cancelled", async () => { - mockClient.deleteMultipleDevices - // require auth - .mockRejectedValueOnce(interactiveAuthError) - // then succeed - .mockResolvedValueOnce({}); - - mockClient.getDevices - .mockResolvedValueOnce({ devices: [ownDevice, verifiedDevice, unverifiedDevice] }) - // pretend it was really deleted on refresh - .mockResolvedValueOnce({ devices: [ownDevice, unverifiedDevice] }); - - const { container, getByTestId } = render(getComponent()); - - await flushPromises(); - - // reset mock count after initial load - mockClient.getDevices.mockClear(); - - toggleDeviceSelection(container, verifiedDevice.device_id); - - act(() => { - fireEvent.click(getByTestId("sign-out-devices-btn")); - }); - - expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy(); - - await flushPromises(); - // modal rendering has some weird sleeps - await sleep(20); - - // close the modal without submission - act(() => { - const modalCloseButton = document.querySelector('[aria-label="Close dialog"]')!; - fireEvent.click(modalCloseButton); - }); - - await flushPromises(); - - // not refreshed - expect(mockClient.getDevices).not.toHaveBeenCalled(); - // spinner removed - expect(container.getElementsByClassName("mx_Spinner").length).toBeFalsy(); - }); - }); -}); diff --git a/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap deleted file mode 100644 index ad12a99c7c..0000000000 --- a/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap +++ /dev/null @@ -1,506 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` device deletion deletes selected devices when interactive auth is required 1`] = ` -HTMLCollection [ -
    -
    -