Allow dismissing 'Key storage out of sync' temporarily (#31455)

This commit is contained in:
Andy Balaam 2026-01-26 10:46:40 +00:00 committed by GitHub
parent 828c4a47a4
commit 0947517746
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 69 additions and 9 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -38,6 +38,7 @@ Please see LICENSE files in the repository root for full details.
grid-template-columns: 22px 1fr;
column-gap: 8px;
row-gap: 4px;
align-items: center;
padding: var(--cpd-space-3x);
&.mx_Toast_hasIcon {
@ -47,10 +48,17 @@ Please see LICENSE files in the repository root for full details.
grid-column: 1;
}
.mx_Toast_title,
.mx_Toast_body {
.mx_Toast_title {
grid-column: 2;
}
.mx_Toast_body {
grid-column: 2 / 4;
}
.mx_Toast_closebutton {
grid-column: 3;
}
}
&:not(.mx_Toast_hasIcon) {
padding-left: 12px;

View File

@ -8,10 +8,12 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import classNames from "classnames";
import { Text } from "@vector-im/compound-web";
import { IconButton, Text } from "@vector-im/compound-web";
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import ToastStore, { type IToast } from "../../stores/ToastStore";
import { _t } from "../../languageHandler";
interface IState {
toasts: IToast<any>[];
@ -47,7 +49,7 @@ export default class ToastContainer extends React.Component<EmptyObject, IState>
let containerClasses;
if (totalCount !== 0) {
const topToast = this.state.toasts[0];
const { title, icon, key, component, className, bodyClassName, props } = topToast;
const { title, icon, key, component, className, bodyClassName, onCloseButtonClicked, props } = topToast;
const bodyClasses = classNames("mx_Toast_body", bodyClassName);
const toastClasses = classNames("mx_Toast_toast", className, {
mx_Toast_hasIcon: !!icon,
@ -61,11 +63,24 @@ export default class ToastContainer extends React.Component<EmptyObject, IState>
let titleElement;
if (title) {
titleElement = (
<div className="mx_Toast_title">
<Text size="lg" weight="semibold" as="h2">
{title}
</Text>
</div>
<>
<div className="mx_Toast_title">
<Text size="lg" weight="semibold" as="h2">
{title}
</Text>
</div>
{onCloseButtonClicked && (
<IconButton
className="mx_Toast_closebutton"
size="28px"
onClick={onCloseButtonClicked}
tooltip={_t("action|close")}
kind="secondary"
>
<CloseIcon />
</IconButton>
)}
</>
);
}

View File

@ -22,6 +22,15 @@ export interface IToast<C extends ComponentClass> {
component: C;
className?: string;
bodyClassName?: string;
/**
* What to do if the user clicks the close button. If this is undefined, the
* close button is not displayed.
*
* Note: the close button is only displayed if the toast has a title (i.e. if {@link title} is truthy).
*/
onCloseButtonClicked?: () => void;
props?: Omit<React.ComponentProps<C>, "toastKey">; // toastKey is injected by ToastContainer
}

View File

@ -65,6 +65,18 @@ const getIcon = (state: DeviceStateForToast): IToast<any>["icon"] => {
}
};
const shouldShowCloseButton = (state: DeviceStateForToast): boolean => {
switch (state) {
case "key_storage_out_of_sync":
case "identity_needs_reset":
return true;
case "set_up_recovery":
case "verify_this_session":
case "turn_on_key_storage":
return false;
}
};
const getSetupCaption = (state: DeviceStateForToast): string => {
switch (state) {
case "set_up_recovery":
@ -299,10 +311,15 @@ export const showToast = (state: DeviceStateForToast): void => {
}
};
const onCloseButtonClicked = shouldShowCloseButton(state)
? () => DeviceListener.sharedInstance().dismissEncryptionSetup()
: undefined;
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: getTitle(state),
icon: getIcon(state),
onCloseButtonClicked,
props: {
description: getDescription(state),
primaryLabel: getSetupCaption(state),

View File

@ -189,6 +189,17 @@ describe("SetupEncryptionToast", () => {
props: { initialEncryptionState: "change_recovery_key" },
});
});
it("should dismiss the toast when the close button is clicked", async () => {
jest.spyOn(DeviceListener.sharedInstance(), "dismissEncryptionSetup");
act(() => showToast("key_storage_out_of_sync"));
const user = userEvent.setup();
await user.click(await screen.findByRole("button", { name: "Close" }));
expect(DeviceListener.sharedInstance().dismissEncryptionSetup).toHaveBeenCalled();
});
});
describe("Turn on key storage", () => {