diff --git a/res/css/views/settings/tabs/_SettingsTab.pcss b/res/css/views/settings/tabs/_SettingsTab.pcss index 868ea4fec504..8b4d17d8e974 100644 --- a/res/css/views/settings/tabs/_SettingsTab.pcss +++ b/res/css/views/settings/tabs/_SettingsTab.pcss @@ -97,3 +97,11 @@ limitations under the License. .mx_SettingsTab_toggleWithDescription { margin-top: $spacing-24; } + +.mx_SettingsTab_sections { + display: grid; + grid-template-columns: 1fr; + grid-gap: $spacing-32; + + padding: 0 $spacing-16; +} diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 6d4fe15fcc48..e15144969d1c 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -35,6 +35,7 @@ import BaseDialog from "./BaseDialog"; import { IDialogProps } from "./IDialogProps"; import SidebarUserSettingsTab from "../settings/tabs/user/SidebarUserSettingsTab"; import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsTab"; +import SessionManagerTab from '../settings/tabs/user/SessionManagerTab'; import { UserTab } from "./UserTab"; interface IProps extends IDialogProps { @@ -43,25 +44,30 @@ interface IProps extends IDialogProps { interface IState { mjolnirEnabled: boolean; + newSessionManagerEnabled: boolean; } export default class UserSettingsDialog extends React.Component { - private mjolnirWatcher: string | undefined; + private settingsWatchers: string[] = []; constructor(props) { super(props); this.state = { mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), + newSessionManagerEnabled: SettingsStore.getValue("feature_new_device_manager"), }; } public componentDidMount(): void { - this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged); + this.settingsWatchers = [ + SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged), + SettingsStore.watchSetting("feature_new_device_manager", null, this.sessionManagerChanged), + ]; } public componentWillUnmount(): void { - this.mjolnirWatcher && SettingsStore.unwatchSetting(this.mjolnirWatcher); + this.settingsWatchers.forEach(watcherRef => SettingsStore.unwatchSetting(watcherRef)); } private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => { @@ -69,6 +75,11 @@ 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() { const tabs: Tab[] = []; @@ -132,6 +143,16 @@ export default class UserSettingsDialog extends React.Component , "UserSettingsSecurityPrivacy", )); + if (this.state.newSessionManagerEnabled) { + tabs.push(new Tab( + UserTab.SessionManager, + _td("Sessions"), + "mx_UserSettingsDialog_securityIcon", + , + // 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") || SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k)) diff --git a/src/components/views/dialogs/UserTab.ts b/src/components/views/dialogs/UserTab.ts index b5b2782c0d0d..ab6a21321177 100644 --- a/src/components/views/dialogs/UserTab.ts +++ b/src/components/views/dialogs/UserTab.ts @@ -26,4 +26,5 @@ export enum UserTab { Labs = "USER_LABS_TAB", Mjolnir = "USER_MJOLNIR_TAB", Help = "USER_HELP_TAB", + SessionManager = "USER_SESSION_MANAGER_TAB", } diff --git a/src/components/views/settings/tabs/SettingsTab.tsx b/src/components/views/settings/tabs/SettingsTab.tsx new file mode 100644 index 000000000000..01a366846d3a --- /dev/null +++ b/src/components/views/settings/tabs/SettingsTab.tsx @@ -0,0 +1,34 @@ +/* +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 Heading from "../../typography/Heading"; + +export interface SettingsTabProps { + heading: string; + children?: React.ReactNode; +} + +const SettingsTab: React.FC = ({ heading, children }) => ( +
+ { heading } +
+ { children } +
+
+); + +export default SettingsTab; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx new file mode 100644 index 000000000000..afa663392ba2 --- /dev/null +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -0,0 +1,26 @@ +/* +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 { _t } from "../../../../../languageHandler"; +import SettingsTab from '../SettingsTab'; + +const SessionManagerTab: React.FC = () => { + return ; +}; + +export default SessionManagerTab; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2ceea07a5d48..b8605842c41b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -904,6 +904,7 @@ "Right-click message context menu": "Right-click message context menu", "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)", + "Use new session manager (under active development)": "Use new session manager (under active development)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", @@ -1565,6 +1566,7 @@ "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.", "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.", + "Sessions": "Sessions", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 73c75d3bb311..ce1840010c32 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -437,6 +437,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td("Favourite Messages (under active development)"), default: false, }, + "feature_new_device_manager": { + isFeature: true, + labsGroup: LabGroup.Experimental, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Use new session manager (under active development)"), + default: false, + }, "baseFontSize": { displayName: _td("Font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, diff --git a/test/components/views/dialogs/UserSettingsDialog-test.tsx b/test/components/views/dialogs/UserSettingsDialog-test.tsx index 50dc3f97aa1c..68385b620483 100644 --- a/test/components/views/dialogs/UserSettingsDialog-test.tsx +++ b/test/components/views/dialogs/UserSettingsDialog-test.tsx @@ -115,6 +115,12 @@ describe('', () => { expect(getByTestId(`settings-tab-${UserTab.Voice}`)).toBeTruthy(); }); + it('renders session manager tab when enabled', () => { + mockSettingsStore.getValue.mockImplementation((settingName) => settingName === "feature_new_device_manager"); + const { getByTestId } = render(getComponent()); + expect(getByTestId(`settings-tab-${UserTab.SessionManager}`)).toBeTruthy(); + }); + it('renders labs tab when show_labs_settings is enabled in config', () => { mockSdkConfig.get.mockImplementation((configName) => configName === "show_labs_settings"); const { getByTestId } = render(getComponent()); @@ -130,11 +136,11 @@ describe('', () => { expect(getByTestId(`settings-tab-${UserTab.Labs}`)).toBeTruthy(); }); - it('watches feature_mjolnir setting', () => { - let watchSettingCallback: CallbackFn = jest.fn(); + it('watches settings', () => { + const watchSettingCallbacks: Record = {}; mockSettingsStore.watchSetting.mockImplementation((settingName, roomId, callback) => { - watchSettingCallback = callback; + watchSettingCallbacks[settingName] = callback; return `mock-watcher-id-${settingName}`; }); @@ -142,16 +148,24 @@ 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 - watchSettingCallback("feature_mjolnir", '', SettingLevel.ACCOUNT, true, true); - + 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 mjolnir after 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/settings/tabs/SettingsTab-test.tsx b/test/components/views/settings/tabs/SettingsTab-test.tsx new file mode 100644 index 000000000000..001162635a59 --- /dev/null +++ b/test/components/views/settings/tabs/SettingsTab-test.tsx @@ -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 React, { ReactElement } from 'react'; +import { render } from '@testing-library/react'; + +import SettingsTab, { SettingsTabProps } from '../../../../../src/components/views/settings/tabs/SettingsTab'; + +describe('', () => { + const getComponent = (props: SettingsTabProps): ReactElement => (); + it('renders tab', () => { + const { container } = render(getComponent({ heading: 'Test Tab', children:
test
})); + + expect(container).toMatchSnapshot(); + }); +}); + diff --git a/test/components/views/settings/tabs/__snapshots__/SettingsTab-test.tsx.snap b/test/components/views/settings/tabs/__snapshots__/SettingsTab-test.tsx.snap new file mode 100644 index 000000000000..704189fb1f21 --- /dev/null +++ b/test/components/views/settings/tabs/__snapshots__/SettingsTab-test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders tab 1`] = ` +
+
+

+ Test Tab +

+
+
+ test +
+
+
+
+`;