mirror of https://github.com/vector-im/riot-web
Add sign out button to settings profile section (#12666)
* Add sign out button to settings profile section And move the logic for displaying the dialog out of the user menu to somewhere it can be re-used. Also close any open dialog on logout, because otherwise, well... you can guess. * Missing import * Update screenshot * This button doesn't need to be an anchor * Use Flex component * Use new force-close function * More testspull/24763/head
parent
844da7a656
commit
c2c108957e
Binary file not shown.
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
@ -27,7 +27,7 @@ import { UserTab } from "../views/dialogs/UserTab";
|
||||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
import LogoutDialog, { shouldShowLogoutDialog } from "../views/dialogs/LogoutDialog";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme";
|
import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme";
|
||||||
import { RovingAccessibleButton } from "../../accessibility/RovingTabIndex";
|
import { RovingAccessibleButton } from "../../accessibility/RovingTabIndex";
|
||||||
|
@ -288,7 +288,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
if (await this.shouldShowLogoutDialog()) {
|
if (await shouldShowLogoutDialog(MatrixClientPeg.safeGet())) {
|
||||||
Modal.createDialog(LogoutDialog);
|
Modal.createDialog(LogoutDialog);
|
||||||
} else {
|
} else {
|
||||||
defaultDispatcher.dispatch({ action: "logout" });
|
defaultDispatcher.dispatch({ action: "logout" });
|
||||||
|
@ -297,27 +297,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the `LogoutDialog` should be shown instead of the simple logout flow.
|
|
||||||
* The `LogoutDialog` will check the crypto recovery status of the account and
|
|
||||||
* help the user setup recovery properly if needed.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async shouldShowLogoutDialog(): Promise<boolean> {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const crypto = cli?.getCrypto();
|
|
||||||
if (!crypto) return false;
|
|
||||||
|
|
||||||
// If any room is encrypted, we need to show the advanced logout flow
|
|
||||||
const allRooms = cli!.getRooms();
|
|
||||||
for (const room of allRooms) {
|
|
||||||
const isE2e = await crypto.isEncryptionEnabledInRoom(room.roomId);
|
|
||||||
if (isE2e) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onSignInClick = (): void => {
|
private onSignInClick = (): void => {
|
||||||
defaultDispatcher.dispatch({ action: "start_login" });
|
defaultDispatcher.dispatch({ action: "start_login" });
|
||||||
this.setState({ contextMenuPosition: null }); // also close the menu
|
this.setState({ contextMenuPosition: null }); // also close the menu
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
|
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
|
||||||
import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog";
|
import type ExportE2eKeysDialog from "../../../async-components/views/dialogs/security/ExportE2eKeysDialog";
|
||||||
|
@ -58,6 +59,25 @@ interface IState {
|
||||||
backupStatus: BackupStatus;
|
backupStatus: BackupStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the `LogoutDialog` should be shown instead of the simple logout flow.
|
||||||
|
* The `LogoutDialog` will check the crypto recovery status of the account and
|
||||||
|
* help the user setup recovery properly if needed.
|
||||||
|
*/
|
||||||
|
export async function shouldShowLogoutDialog(cli: MatrixClient): Promise<boolean> {
|
||||||
|
const crypto = cli?.getCrypto();
|
||||||
|
if (!crypto) return false;
|
||||||
|
|
||||||
|
// If any room is encrypted, we need to show the advanced logout flow
|
||||||
|
const allRooms = cli!.getRooms();
|
||||||
|
for (const room of allRooms) {
|
||||||
|
const isE2e = await crypto.isEncryptionEnabledInRoom(room.roomId);
|
||||||
|
if (isE2e) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export default class LogoutDialog extends React.Component<IProps, IState> {
|
export default class LogoutDialog extends React.Component<IProps, IState> {
|
||||||
public static defaultProps = {
|
public static defaultProps = {
|
||||||
onFinished: function () {},
|
onFinished: function () {},
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from "r
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web";
|
import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web";
|
||||||
import { Icon as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg";
|
import { Icon as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg";
|
||||||
|
import { Icon as SignOutIcon } from "@vector-im/compound-design-tokens/icons/sign-out.svg";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||||
|
@ -31,6 +32,10 @@ import { useId } from "../../../utils/useId";
|
||||||
import CopyableText from "../elements/CopyableText";
|
import CopyableText from "../elements/CopyableText";
|
||||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import { Flex } from "../../utils/Flex";
|
||||||
|
|
||||||
const SpinnerToast: React.FC = ({ children }) => (
|
const SpinnerToast: React.FC = ({ children }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -76,6 +81,25 @@ const ManageAccountButton: React.FC<ManageAccountButtonProps> = ({ externalAccou
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SignOutButton: React.FC = () => {
|
||||||
|
const client = useMatrixClientContext();
|
||||||
|
|
||||||
|
const onClick = useCallback(async () => {
|
||||||
|
if (await shouldShowLogoutDialog(client)) {
|
||||||
|
Modal.createDialog(LogoutDialog);
|
||||||
|
} else {
|
||||||
|
defaultDispatcher.dispatch({ action: "logout" });
|
||||||
|
}
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleButton onClick={onClick} kind="danger_outline">
|
||||||
|
<SignOutIcon className="mx_UserProfileSettings_accountmanageIcon" width="24" height="24" />
|
||||||
|
{_t("action|sign_out")}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface UserProfileSettingsProps {
|
interface UserProfileSettingsProps {
|
||||||
// The URL to redirect the user to in order to manage their account.
|
// The URL to redirect the user to in order to manage their account.
|
||||||
externalAccountManagementUrl?: string;
|
externalAccountManagementUrl?: string;
|
||||||
|
@ -219,11 +243,12 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
||||||
|
<Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
|
||||||
{externalAccountManagementUrl && (
|
{externalAccountManagementUrl && (
|
||||||
<div className="mx_UserProfileSettings_profile_buttons">
|
|
||||||
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
|
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<SignOutButton />
|
||||||
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,12 +18,15 @@ import React, { ChangeEvent } from "react";
|
||||||
import { act, render, screen } from "@testing-library/react";
|
import { act, render, screen } from "@testing-library/react";
|
||||||
import { MatrixClient, UploadResponse } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, UploadResponse } from "matrix-js-sdk/src/matrix";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import UserProfileSettings from "../../../../src/components/views/settings/UserProfileSettings";
|
import UserProfileSettings from "../../../../src/components/views/settings/UserProfileSettings";
|
||||||
import { stubClient } from "../../../test-utils";
|
import { mkStubRoom, stubClient } from "../../../test-utils";
|
||||||
import { ToastContext, ToastRack } from "../../../../src/contexts/ToastContext";
|
import { ToastContext, ToastRack } from "../../../../src/contexts/ToastContext";
|
||||||
import { OwnProfileStore } from "../../../../src/stores/OwnProfileStore";
|
import { OwnProfileStore } from "../../../../src/stores/OwnProfileStore";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import dis from "../../../../src/dispatcher/dispatcher";
|
||||||
|
import Modal from "../../../../src/Modal";
|
||||||
|
|
||||||
interface MockedAvatarSettingProps {
|
interface MockedAvatarSettingProps {
|
||||||
removeAvatar: () => void;
|
removeAvatar: () => void;
|
||||||
|
@ -43,6 +46,11 @@ jest.mock(
|
||||||
}) as React.FC<MockedAvatarSettingProps>,
|
}) as React.FC<MockedAvatarSettingProps>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.mock("../../../../src/dispatcher/dispatcher", () => ({
|
||||||
|
dispatch: jest.fn(),
|
||||||
|
register: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
let editInPlaceOnChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
let editInPlaceOnChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
let editInPlaceOnSave: () => void;
|
let editInPlaceOnSave: () => void;
|
||||||
let editInPlaceOnCancel: () => void;
|
let editInPlaceOnCancel: () => void;
|
||||||
|
@ -209,4 +217,30 @@ describe("ProfileSettings", () => {
|
||||||
|
|
||||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("signs out directly if no rooms are encrypted", async () => {
|
||||||
|
renderProfileSettings(toastRack, client);
|
||||||
|
|
||||||
|
const signOutButton = await screen.findByText("Sign out");
|
||||||
|
await userEvent.click(signOutButton);
|
||||||
|
|
||||||
|
expect(dis.dispatch).toHaveBeenCalledWith({ action: "logout" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays confirmation dialog if rooms are encrypted", async () => {
|
||||||
|
jest.spyOn(Modal, "createDialog");
|
||||||
|
|
||||||
|
const mockRoom = mkStubRoom("!test:room", "Test Room", client);
|
||||||
|
client.getRooms = jest.fn().mockReturnValue([mockRoom]);
|
||||||
|
client.getCrypto = jest.fn().mockReturnValue({
|
||||||
|
isEncryptionEnabledInRoom: jest.fn().mockReturnValue(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
renderProfileSettings(toastRack, client);
|
||||||
|
|
||||||
|
const signOutButton = await screen.findByText("Sign out");
|
||||||
|
await userEvent.click(signOutButton);
|
||||||
|
|
||||||
|
expect(Modal.createDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue