diff --git a/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png b/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png index f5514af8cf..09db9d3b5c 100644 Binary files a/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png and b/playwright/snapshots/crypto/toasts.spec.ts/key-storage-out-of-sync-toast-linux.png differ diff --git a/res/css/structures/_ToastContainer.pcss b/res/css/structures/_ToastContainer.pcss index 255aef685c..6f21e495d6 100644 --- a/res/css/structures/_ToastContainer.pcss +++ b/res/css/structures/_ToastContainer.pcss @@ -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; diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 21245dbe2f..16275d9d8e 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -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[]; @@ -47,7 +49,7 @@ export default class ToastContainer extends React.Component 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 let titleElement; if (title) { titleElement = ( -
- - {title} - -
+ <> +
+ + {title} + +
+ {onCloseButtonClicked && ( + + + + )} + ); } diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 85888ee30f..971e49be58 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -22,6 +22,15 @@ export interface IToast { 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, "toastKey">; // toastKey is injected by ToastContainer } diff --git a/src/toasts/SetupEncryptionToast.tsx b/src/toasts/SetupEncryptionToast.tsx index 1b5f8a4a49..93b97db365 100644 --- a/src/toasts/SetupEncryptionToast.tsx +++ b/src/toasts/SetupEncryptionToast.tsx @@ -65,6 +65,18 @@ const getIcon = (state: DeviceStateForToast): IToast["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), diff --git a/test/unit-tests/toasts/SetupEncryptionToast-test.tsx b/test/unit-tests/toasts/SetupEncryptionToast-test.tsx index c96e5c1673..52c19984bc 100644 --- a/test/unit-tests/toasts/SetupEncryptionToast-test.tsx +++ b/test/unit-tests/toasts/SetupEncryptionToast-test.tsx @@ -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", () => {