From 87d3fbd9961436426ff6ec7f59d3ec8cd8ef1e43 Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 11 Oct 2022 11:10:55 +0200 Subject: [PATCH] Device manager - promote to beta (#9380) * promote new session manager to beta * hide old sessions section when new dm enabled * use correct logic * add new ViewUserDeviceSettings action * replace device management ctas with viewUserDeviceSettings * test SecurityUserSettingsTab * more complete mocks * more thorough mocks * more mocks * test LabsUserSettingsTab * lint * updated copy * update snaps for new copy --- .../handlers/viewUserDeviceSettings.ts | 30 +++ src/components/structures/MatrixChat.tsx | 5 + src/components/views/right_panel/UserInfo.tsx | 4 +- .../tabs/user/LabsUserSettingsTab.tsx | 11 +- .../tabs/user/SecurityUserSettingsTab.tsx | 20 +- src/dispatcher/actions.ts | 5 + src/i18n/strings/en_EN.json | 5 +- src/settings/Settings.tsx | 18 +- src/toasts/BulkUnverifiedSessionsToast.ts | 4 +- src/toasts/UnverifiedSessionToast.ts | 4 +- .../handlers/viewUserDeviceSettings-test.ts | 48 +++++ .../tabs/user/LabsUserSettingsTab-test.tsx | 73 +++++++ .../user/SecurityUserSettingsTab-test.tsx | 68 ++++++ .../LabsUserSettingsTab-test.tsx.snap | 196 ++++++++++++++++++ test/test-utils/client.ts | 32 ++- 15 files changed, 504 insertions(+), 19 deletions(-) create mode 100644 src/actions/handlers/viewUserDeviceSettings.ts create mode 100644 test/actions/handlers/viewUserDeviceSettings-test.ts create mode 100644 test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx create mode 100644 test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx create mode 100644 test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap diff --git a/src/actions/handlers/viewUserDeviceSettings.ts b/src/actions/handlers/viewUserDeviceSettings.ts new file mode 100644 index 0000000000..e1dc7b3f26 --- /dev/null +++ b/src/actions/handlers/viewUserDeviceSettings.ts @@ -0,0 +1,30 @@ +/* +Copyright 2022 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 { UserTab } from "../../components/views/dialogs/UserTab"; +import { Action } from "../../dispatcher/actions"; +import defaultDispatcher from "../../dispatcher/dispatcher"; + +/** + * Redirect to the correct device manager section + * Based on the labs setting + */ +export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean) => { + defaultDispatcher.dispatch({ + action: Action.ViewUserSettings, + initialTabId: isNewDeviceManagerEnabled ? UserTab.SessionManager : UserTab.Security, + }); +}; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 6dd2820aa1..923f461092 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -137,6 +137,7 @@ import { TimelineRenderingType } from "../../contexts/RoomContext"; import { UseCaseSelection } from '../views/elements/UseCaseSelection'; import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig'; import { isLocalRoom } from '../../utils/localRoom/isLocalRoom'; +import { viewUserDeviceSettings } from '../../actions/handlers/viewUserDeviceSettings'; // legacy export export { default as Views } from "../../Views"; @@ -677,6 +678,10 @@ export default class MatrixChat extends React.PureComponent { } break; } + case Action.ViewUserDeviceSettings: { + viewUserDeviceSettings(SettingsStore.getValue("feature_new_device_manager")); + break; + } case Action.ViewUserSettings: { const tabPayload = payload as OpenToTabPayload; Modal.createDialog(UserSettingsDialog, diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 45489603ba..810ae48dd7 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -48,7 +48,6 @@ import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; import { Action } from "../../../dispatcher/actions"; -import { UserTab } from "../dialogs/UserTab"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import { E2EStatus } from "../../../utils/ShieldUtils"; @@ -1331,8 +1330,7 @@ const BasicUserInfo: React.FC<{ className="mx_UserInfo_field" onClick={() => { dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + action: Action.ViewUserDeviceSettings, }); }} > diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx index 80e2ebb6cf..6057587626 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx @@ -80,7 +80,10 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> { let betaSection; if (betas.length) { - betaSection =
+ betaSection =
{ betas.map(f => ) }
; } @@ -137,7 +140,11 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> { labsSections = <> { sortBy(Array.from(groups.entries()), "0").map(([group, flags]) => ( -
+
{ _t(labGroupNames[group]) } { flags }
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx index 91b448eb3b..f4e4e55513 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx @@ -346,19 +346,29 @@ export default class SecurityUserSettingsTab extends React.Component - { warning } + const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager"); + const devicesSection = useNewSessionManager + ? null + : <>
{ _t("Where you're signed in") }
-
+
{ _t( "Manage your signed-in devices below. " + - "A device's name is visible to people you communicate with.", + "A device's name is visible to people you communicate with.", ) }
+ ; + + return ( +
+ { warning } + { devicesSection }
{ _t("Encryption") }
{ secureBackup } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 4e161a7005..2b2e443e81 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -40,6 +40,11 @@ export enum Action { */ ViewUserSettings = "view_user_settings", + /** + * Open the user device settings. No additional payload information required. + */ + ViewUserDeviceSettings = "view_user_device_settings", + /** * Opens the room directory. No additional payload information required. */ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 737f3e8879..5bd66ec9a3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -922,7 +922,10 @@ "Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)", "Favourite Messages (under active development)": "Favourite Messages (under active development)", "Voice broadcast (under active development)": "Voice broadcast (under active development)", - "Use new session manager (under active development)": "Use new session manager (under active development)", + "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.", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a4e55e6fcd..60fd06ef85 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -475,8 +475,24 @@ export const SETTINGS: {[setting: string]: ISetting} = { isFeature: true, labsGroup: LabGroup.Experimental, supportedLevels: LEVELS_FEATURE, - displayName: _td("Use new session manager (under active development)"), + displayName: _td("Use new session manager"), default: false, + betaInfo: { + title: _td('New session manager'), + caption: () => <> +

+ { _td('Have greater visibility and control over all your sessions.') } +

+

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

+ + , + }, }, "baseFontSize": { displayName: _td("Font size"), diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index 6ddb0d7db5..0113f2f030 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -20,7 +20,6 @@ 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/UserTab"; const TOAST_KEY = "reviewsessions"; @@ -29,8 +28,7 @@ export const showToast = (deviceIds: Set) => { DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds); dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + action: Action.ViewUserDeviceSettings, }); }; diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index d0db97cd08..f2d637ef0d 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -21,7 +21,6 @@ import DeviceListener from '../DeviceListener'; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; import { Action } from "../dispatcher/actions"; -import { UserTab } from "../components/views/dialogs/UserTab"; function toastKey(deviceId: string) { return "unverified_session_" + deviceId; @@ -33,8 +32,7 @@ export const showToast = async (deviceId: string) => { const onAccept = () => { DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + action: Action.ViewUserDeviceSettings, }); }; diff --git a/test/actions/handlers/viewUserDeviceSettings-test.ts b/test/actions/handlers/viewUserDeviceSettings-test.ts new file mode 100644 index 0000000000..72d1db430d --- /dev/null +++ b/test/actions/handlers/viewUserDeviceSettings-test.ts @@ -0,0 +1,48 @@ +/* +Copyright 2022 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 { viewUserDeviceSettings } from "../../../src/actions/handlers/viewUserDeviceSettings"; +import { UserTab } from "../../../src/components/views/dialogs/UserTab"; +import { Action } from "../../../src/dispatcher/actions"; +import defaultDispatcher from "../../../src/dispatcher/dispatcher"; + +describe('viewUserDeviceSettings()', () => { + const dispatchSpy = jest.spyOn(defaultDispatcher, 'dispatch'); + + beforeEach(() => { + dispatchSpy.mockClear(); + }); + + it('dispatches action to view new session manager when enabled', () => { + const isNewDeviceManagerEnabled = true; + viewUserDeviceSettings(isNewDeviceManagerEnabled); + + 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/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx new file mode 100644 index 0000000000..614bb4062f --- /dev/null +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -0,0 +1,73 @@ +/* +Copyright 2022 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 { render } from '@testing-library/react'; + +import LabsUserSettingsTab from '../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab'; +import SettingsStore from '../../../../../../src/settings/SettingsStore'; +import { + getMockClientWithEventEmitter, + mockClientMethodsServer, + mockClientMethodsUser, +} from '../../../../../test-utils'; +import SdkConfig from '../../../../../../src/SdkConfig'; + +describe('', () => { + const sdkConfigSpy = jest.spyOn(SdkConfig, 'get'); + + const defaultProps = { + closeSettingsFn: jest.fn(), + }; + const getComponent = () => ; + + const userId = '@alice:server.org'; + getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + }); + + const settingsValueSpy = jest.spyOn(SettingsStore, 'getValue'); + + beforeEach(() => { + jest.clearAllMocks(); + settingsValueSpy.mockReturnValue(false); + sdkConfigSpy.mockReturnValue(false); + }); + + it('renders settings marked as beta as beta cards', () => { + const { getByTestId } = render(getComponent()); + expect(getByTestId("labs-beta-section")).toMatchSnapshot(); + }); + + it('does not render non-beta labs settings when disabled in config', () => { + const { container } = render(getComponent()); + expect(sdkConfigSpy).toHaveBeenCalledWith('show_labs_settings'); + + const labsSections = container.getElementsByClassName('mx_SettingsTab_section'); + // only section is beta section + expect(labsSections.length).toEqual(1); + }); + + it('renders non-beta labs settings when enabled in config', () => { + // enable labs + sdkConfigSpy.mockImplementation(configName => configName === 'show_labs_settings'); + const { container } = render(getComponent()); + + const labsSections = container.getElementsByClassName('mx_SettingsTab_section'); + expect(labsSections.length).toEqual(11); + }); +}); diff --git a/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx new file mode 100644 index 0000000000..bddb493463 --- /dev/null +++ b/test/components/views/settings/tabs/user/SecurityUserSettingsTab-test.tsx @@ -0,0 +1,68 @@ +/* +Copyright 2022 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 { render } from '@testing-library/react'; +import React from 'react'; + +import SecurityUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab"; +import SettingsStore from '../../../../../../src/settings/SettingsStore'; +import { + getMockClientWithEventEmitter, + mockClientMethodsServer, + mockClientMethodsUser, + mockClientMethodsCrypto, + mockClientMethodsDevice, + mockPlatformPeg, +} from '../../../../../test-utils'; + +describe('', () => { + const defaultProps = { + closeSettingsFn: jest.fn(), + }; + const getComponent = () => ; + + const userId = '@alice:server.org'; + const deviceId = 'alices-device'; + getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + ...mockClientMethodsDevice(deviceId), + ...mockClientMethodsCrypto(), + getRooms: jest.fn().mockReturnValue([]), + getIgnoredUsers: jest.fn(), + }); + + const settingsValueSpy = jest.spyOn(SettingsStore, 'getValue'); + + beforeEach(() => { + mockPlatformPeg(); + jest.clearAllMocks(); + settingsValueSpy.mockReturnValue(false); + }); + + it('renders sessions section when new session manager is disabled', () => { + settingsValueSpy.mockReturnValue(false); + const { getByTestId } = render(getComponent()); + + expect(getByTestId('devices-section')).toBeTruthy(); + }); + + it('does not render sessions section when new session manager is enabled', () => { + settingsValueSpy.mockReturnValue(true); + const { queryByTestId } = render(getComponent()); + + expect(queryByTestId('devices-section')).toBeFalsy(); + }); +}); diff --git a/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap new file mode 100644 index 0000000000..b2ff84a823 --- /dev/null +++ b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders settings marked as beta as beta cards 1`] = ` +
+
+
+
+

+ + Video rooms + + + Beta + +

+
+

+ A new way to chat over voice and video in . +

+

+ Video rooms are always-on VoIP channels embedded within a room in . +

+
+
+
+ Join the beta +
+
+
+ Joining the beta will reload . +
+
+
+
+ +
+
+
+
+
+
+

+ + Threads + + + Beta + +

+
+

+ Keep discussions organised with threads. +

+

+ + Threads help keep conversations on-topic and easy to track. + + Learn more + + . + +

+
+
+
+ Join the beta +
+
+
+ Joining the beta will reload . +
+
+
+
+ +
+
+
+
+
+
+

+ + New session manager + + + Beta + +

+
+

+ 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. +

+
+
+
+ Join the beta +
+
+
+
+ +
+
+
+
+`; diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts index 4a5b318491..d6ec3f2fd3 100644 --- a/test/test-utils/client.ts +++ b/test/test-utils/client.ts @@ -15,7 +15,7 @@ limitations under the License. */ import EventEmitter from "events"; -import { MethodKeysOf, mocked, MockedObject } from "jest-mock"; +import { MethodKeysOf, mocked, MockedObject, PropertyKeysOf } from "jest-mock"; import { MatrixClient, User } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; @@ -71,6 +71,7 @@ export const mockClientMethodsUser = (userId = '@alice:domain') => ({ credentials: { userId }, getThreePids: jest.fn().mockResolvedValue({ threepids: [] }), getAccessToken: jest.fn(), + getDeviceId: jest.fn(), }); /** @@ -94,6 +95,35 @@ export const mockClientMethodsServer = (): Partial, unknown>> => ({ + getDeviceId: jest.fn().mockReturnValue(deviceId), + getDeviceEd25519Key: jest.fn(), + getDevices: jest.fn().mockResolvedValue({ devices: [] }), +}); + +export const mockClientMethodsCrypto = (): Partial & PropertyKeysOf, unknown> +> => ({ + isCryptoEnabled: jest.fn(), + isSecretStorageReady: jest.fn(), + isCrossSigningReady: jest.fn(), + isKeyBackupKeyStored: jest.fn(), + getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }), + getStoredCrossSigningForUser: jest.fn(), + checkKeyBackup: jest.fn().mockReturnValue({}), + crypto: { + getSessionBackupPrivateKey: jest.fn(), + secretStorage: { hasKey: jest.fn() }, + crossSigningInfo: { + getId: jest.fn(), + isStoredInSecretStorage: jest.fn(), + }, + }, +}); +