mirror of https://github.com/vector-im/riot-web
				
				
				
			Device manager - contextual menus (#9832)
* add session count to current session contextual signout * add contextual menu to other sessions section * typo * i18n * strictpull/28788/head^2
							parent
							
								
									2e097a00c7
								
							
						
					
					
						commit
						c0ba1b8a1c
					
				| 
						 | 
				
			
			@ -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<void> | undefined;
 | 
			
		||||
    onVerifyCurrentDevice: () => void;
 | 
			
		||||
    onSignOutCurrentDevice: () => void;
 | 
			
		||||
| 
						 | 
				
			
			@ -41,13 +44,17 @@ interface Props {
 | 
			
		|||
    saveDeviceName: (deviceName: string) => Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CurrentDeviceSectionHeadingProps = Pick<Props, "onSignOutCurrentDevice" | "signOutAllOtherSessions"> & {
 | 
			
		||||
type CurrentDeviceSectionHeadingProps = Pick<
 | 
			
		||||
    Props,
 | 
			
		||||
    "onSignOutCurrentDevice" | "signOutAllOtherSessions" | "otherSessionsCount"
 | 
			
		||||
> & {
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> = ({
 | 
			
		||||
    onSignOutCurrentDevice,
 | 
			
		||||
    signOutAllOtherSessions,
 | 
			
		||||
    otherSessionsCount,
 | 
			
		||||
    disabled,
 | 
			
		||||
}) => {
 | 
			
		||||
    const menuOptions = [
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +68,7 @@ const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> =
 | 
			
		|||
            ? [
 | 
			
		||||
                  <IconizedContextMenuOption
 | 
			
		||||
                      key="sign-out-all-others"
 | 
			
		||||
                      label={_t("Sign out all other sessions")}
 | 
			
		||||
                      label={_t("Sign out of all other sessions (%(otherSessionsCount)s)", { otherSessionsCount })}
 | 
			
		||||
                      onClick={signOutAllOtherSessions}
 | 
			
		||||
                      isDestructive
 | 
			
		||||
                  />,
 | 
			
		||||
| 
						 | 
				
			
			@ -85,6 +92,7 @@ const CurrentDeviceSection: React.FC<Props> = ({
 | 
			
		|||
    isLoading,
 | 
			
		||||
    isSigningOut,
 | 
			
		||||
    localNotificationSettings,
 | 
			
		||||
    otherSessionsCount,
 | 
			
		||||
    setPushNotifications,
 | 
			
		||||
    onVerifyCurrentDevice,
 | 
			
		||||
    onSignOutCurrentDevice,
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +108,7 @@ const CurrentDeviceSection: React.FC<Props> = ({
 | 
			
		|||
                <CurrentDeviceSectionHeading
 | 
			
		||||
                    onSignOutCurrentDevice={onSignOutCurrentDevice}
 | 
			
		||||
                    signOutAllOtherSessions={signOutAllOtherSessions}
 | 
			
		||||
                    otherSessionsCount={otherSessionsCount}
 | 
			
		||||
                    disabled={isLoading || !device || isSigningOut}
 | 
			
		||||
                />
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Props> = ({
 | 
			
		||||
    otherSessionsCount,
 | 
			
		||||
    disabled,
 | 
			
		||||
    signOutAllOtherSessions,
 | 
			
		||||
}) => {
 | 
			
		||||
    const menuOptions = [
 | 
			
		||||
        <IconizedContextMenuOption
 | 
			
		||||
            key="sign-out-all-others"
 | 
			
		||||
            label={_t("Sign out of %(count)s sessions", { count: otherSessionsCount })}
 | 
			
		||||
            onClick={signOutAllOtherSessions}
 | 
			
		||||
            isDestructive
 | 
			
		||||
        />,
 | 
			
		||||
    ];
 | 
			
		||||
    return (
 | 
			
		||||
        <SettingsSubsectionHeading heading={_t("Other sessions")}>
 | 
			
		||||
            <KebabContextMenu
 | 
			
		||||
                disabled={disabled}
 | 
			
		||||
                title={_t("Options")}
 | 
			
		||||
                options={menuOptions}
 | 
			
		||||
                data-testid="other-sessions-menu"
 | 
			
		||||
            />
 | 
			
		||||
        </SettingsSubsectionHeading>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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<boolean> => {
 | 
			
		||||
    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 && (
 | 
			
		||||
                <SettingsSubsection
 | 
			
		||||
                    heading={_t("Other sessions")}
 | 
			
		||||
                    heading={
 | 
			
		||||
                        <OtherSessionsSectionHeading
 | 
			
		||||
                            otherSessionsCount={otherSessionsCount}
 | 
			
		||||
                            signOutAllOtherSessions={signOutAllOtherSessions!}
 | 
			
		||||
                            disabled={!!signingOutDeviceIds.length}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                    description={_t(
 | 
			
		||||
                        `For best security, verify your sessions and sign out ` +
 | 
			
		||||
                            `from any session that you don't recognize or use anymore.`,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,6 +42,7 @@ describe("<CurrentDeviceSection />", () => {
 | 
			
		|||
        saveDeviceName: jest.fn(),
 | 
			
		||||
        isLoading: false,
 | 
			
		||||
        isSigningOut: false,
 | 
			
		||||
        otherSessionsCount: 1,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getComponent = (props = {}): React.ReactElement => <CurrentDeviceSection {...defaultProps} {...props} />;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -643,7 +643,7 @@ describe("<SessionManagerTab />", () => {
 | 
			
		|||
            });
 | 
			
		||||
 | 
			
		||||
            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("<SessionManagerTab />", () => {
 | 
			
		|||
            });
 | 
			
		||||
 | 
			
		||||
            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("<SessionManagerTab />", () => {
 | 
			
		|||
 | 
			
		||||
                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,
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue