diff --git a/src/components/views/settings/devices/CurrentDeviceSection.tsx b/src/components/views/settings/devices/CurrentDeviceSection.tsx index 46c6f3e7d12..db56a07e0e1 100644 --- a/src/components/views/settings/devices/CurrentDeviceSection.tsx +++ b/src/components/views/settings/devices/CurrentDeviceSection.tsx @@ -34,6 +34,9 @@ interface Props { isLoading: boolean; isSigningOut: boolean; localNotificationSettings?: LocalNotificationSettings | undefined; + // number of other sessions the user has + // excludes current session + otherSessionsCount: number; setPushNotifications?: (deviceId: string, enabled: boolean) => Promise | undefined; onVerifyCurrentDevice: () => void; onSignOutCurrentDevice: () => void; @@ -41,13 +44,17 @@ interface Props { saveDeviceName: (deviceName: string) => Promise; } -type CurrentDeviceSectionHeadingProps = Pick & { +type CurrentDeviceSectionHeadingProps = Pick< + Props, + "onSignOutCurrentDevice" | "signOutAllOtherSessions" | "otherSessionsCount" +> & { disabled?: boolean; }; const CurrentDeviceSectionHeading: React.FC = ({ onSignOutCurrentDevice, signOutAllOtherSessions, + otherSessionsCount, disabled, }) => { const menuOptions = [ @@ -61,7 +68,7 @@ const CurrentDeviceSectionHeading: React.FC = ? [ , @@ -85,6 +92,7 @@ const CurrentDeviceSection: React.FC = ({ isLoading, isSigningOut, localNotificationSettings, + otherSessionsCount, setPushNotifications, onVerifyCurrentDevice, onSignOutCurrentDevice, @@ -100,6 +108,7 @@ const CurrentDeviceSection: React.FC = ({ } diff --git a/src/components/views/settings/devices/OtherSessionsSectionHeading.tsx b/src/components/views/settings/devices/OtherSessionsSectionHeading.tsx new file mode 100644 index 00000000000..f2ab2ac3207 --- /dev/null +++ b/src/components/views/settings/devices/OtherSessionsSectionHeading.tsx @@ -0,0 +1,56 @@ +/* +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 { KebabContextMenu } from "../../context_menus/KebabContextMenu"; +import { SettingsSubsectionHeading } from "../shared/SettingsSubsectionHeading"; +import { IconizedContextMenuOption } from "../../context_menus/IconizedContextMenu"; + +interface Props { + // total count of other sessions + // excludes current sessions + // not affected by filters + otherSessionsCount: number; + disabled?: boolean; + signOutAllOtherSessions: () => void; +} + +export const OtherSessionsSectionHeading: React.FC = ({ + otherSessionsCount, + disabled, + signOutAllOtherSessions, +}) => { + const menuOptions = [ + , + ]; + return ( + + + + ); +}; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 1529d9a4369..349ccaeb8f0 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -38,6 +38,7 @@ import SettingsStore from "../../../../../settings/SettingsStore"; import { useAsyncMemo } from "../../../../../hooks/useAsyncMemo"; import QuestionDialog from "../../../dialogs/QuestionDialog"; import { FilterVariation } from "../../devices/filter"; +import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionHeading"; const confirmSignOut = async (sessionsToSignOutCount: number): Promise => { const { finished } = Modal.createDialog(QuestionDialog, { @@ -156,7 +157,8 @@ const SessionManagerTab: React.FC = () => { }; const { [currentDeviceId]: currentDevice, ...otherDevices } = devices; - const shouldShowOtherSessions = Object.keys(otherDevices).length > 0; + const otherSessionsCount = Object.keys(otherDevices).length; + const shouldShowOtherSessions = otherSessionsCount > 0; const onVerifyCurrentDevice = () => { Modal.createDialog(SetupEncryptionDialog as unknown as React.ComponentType, { onFinished: refreshDevices }); @@ -241,10 +243,17 @@ const SessionManagerTab: React.FC = () => { onVerifyCurrentDevice={onVerifyCurrentDevice} onSignOutCurrentDevice={onSignOutCurrentDevice} signOutAllOtherSessions={signOutAllOtherSessions} + otherSessionsCount={otherSessionsCount} /> {shouldShowOtherSessions && ( + } description={_t( `For best security, verify your sessions and sign out ` + `from any session that you don't recognize or use anymore.`, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e2b609d3138..87a7c5c7218 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1627,7 +1627,6 @@ "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?", - "Other sessions": "Other sessions", "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", @@ -1765,7 +1764,7 @@ "Please enter verification code sent via text.": "Please enter verification code sent via text.", "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", - "Sign out all other sessions": "Sign out all other sessions", + "Sign out of all other sessions (%(otherSessionsCount)s)": "Sign out of all other sessions (%(otherSessionsCount)s)", "Current session": "Current session", "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.", @@ -1845,6 +1844,9 @@ "Sign in with QR code": "Sign in with QR code", "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", "Show QR code": "Show QR code", + "Sign out of %(count)s sessions|other": "Sign out of %(count)s sessions", + "Sign out of %(count)s sessions|one": "Sign out of %(count)s session", + "Other sessions": "Other sessions", "Security recommendations": "Security recommendations", "Improve your account security by following these recommendations.": "Improve your account security by following these recommendations.", "View all": "View all", diff --git a/test/components/views/settings/devices/CurrentDeviceSection-test.tsx b/test/components/views/settings/devices/CurrentDeviceSection-test.tsx index f51fd51386a..2c6557337b3 100644 --- a/test/components/views/settings/devices/CurrentDeviceSection-test.tsx +++ b/test/components/views/settings/devices/CurrentDeviceSection-test.tsx @@ -42,6 +42,7 @@ describe("", () => { saveDeviceName: jest.fn(), isLoading: false, isSigningOut: false, + otherSessionsCount: 1, }; const getComponent = (props = {}): React.ReactElement => ; diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index a719bd8fb3d..a90c37c3883 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -643,7 +643,7 @@ describe("", () => { }); fireEvent.click(getByTestId("current-session-menu")); - expect(queryByLabelText("Sign out all other sessions")).toBeFalsy(); + expect(queryByLabelText("Sign out of all other sessions")).toBeFalsy(); }); it("signs out of all other devices from current session context menu", async () => { @@ -657,7 +657,7 @@ describe("", () => { }); fireEvent.click(getByTestId("current-session-menu")); - fireEvent.click(getByLabelText("Sign out all other sessions")); + fireEvent.click(getByLabelText("Sign out of all other sessions (2)")); await confirmSignout(getByTestId); // other devices deleted, excluding current device @@ -928,6 +928,27 @@ describe("", () => { resolveDeleteRequest?.(); }); + + it("signs out of all other devices from other sessions context menu", async () => { + mockClient.getDevices.mockResolvedValue({ + devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice], + }); + const { getByTestId, getByLabelText } = render(getComponent()); + + await act(async () => { + await flushPromises(); + }); + + fireEvent.click(getByTestId("other-sessions-menu")); + fireEvent.click(getByLabelText("Sign out of 2 sessions")); + await confirmSignout(getByTestId); + + // other devices deleted, excluding current device + expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith( + [alicesMobileDevice.device_id, alicesOlderMobileDevice.device_id], + undefined, + ); + }); }); });