From 25d24d478fc649ae45500d637a833672fc149472 Mon Sep 17 00:00:00 2001 From: rbondesson Date: Fri, 30 Jan 2026 13:44:23 +0100 Subject: [PATCH] Refactor DecryptionFailureBody using MVVM and move to shared-components (#31829) * Refactor DecryptionFailureBody to MVVM and moving it to shared components * Added unit test for DecryptionFailureBodyViewModel * Removing the dependency to matrix.js-sdk from the shared component * Kepp class mx_EventTile_content for tile layout * Required changes after rebase * Updates after PR review requests * Clean up unused translation tags in element-web * Added missing unit tests to improve coverage * Additional unit tests to improve test coverage * Removing obsolete tests from the snap * Only listen to verification state changes in the wrapper components and also limit the view model to only allow updates in verification state. * Updates after review requests * Updated and added missing playwright snapshots * Bettter structure on view model --------- Co-authored-by: Florian Duros Co-authored-by: Zack --- .../default-auto.png | Bin 0 -> 6924 bytes ...-backup-configured-verified-false-auto.png | Bin 0 -> 9736 bytes ...s-backup-configured-verified-true-auto.png | Bin 0 -> 6924 bytes .../has-error-block-icon-auto.png | Bin 0 -> 7816 bytes .../has-error-class-name-auto.png | Bin 0 -> 7225 bytes .../has-extra-class-names-auto.png | Bin 0 -> 6924 bytes .../src/i18n/strings/en_EN.json | 9 + packages/shared-components/src/index.ts | 2 +- .../DecryptionFailureBodyView.module.css | 26 +++ .../DecryptionFailureBodyView.stories.tsx | 81 ++++++++ .../DecryptionFailureBodyView.test.tsx | 149 ++++++++++++++ .../DecryptionFailureBodyView.tsx | 176 +++++++++++++++++ .../DecryptionFailureBodyView.test.tsx.snap | 187 ++++++++++++++++++ .../DecryptionFailureBodyView/index.tsx | 13 ++ res/css/_components.pcss | 1 - .../messages/_DecryptionFailureBody.pcss | 22 --- .../MatrixClientContextProvider.tsx | 2 +- .../views/messages/DecryptionFailureBody.tsx | 84 -------- .../views/messages/MessageEvent.tsx | 27 ++- src/components/views/rooms/EventTile.tsx | 28 ++- .../LocalDeviceVerificationStateContext.ts | 13 +- src/i18n/strings/en_EN.json | 5 - .../DecryptionFailureBodyViewModel.ts | 100 ++++++++++ .../MatrixClientContextProvider-test.tsx | 2 +- .../messages/DecryptionFailureBody-test.tsx | 134 ------------- .../DecryptionFailureBody-test.tsx.snap | 45 ----- .../DecryptionFailureBodyViewModel-test.tsx | 101 ++++++++++ 27 files changed, 899 insertions(+), 308 deletions(-) create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/default-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-false-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-backup-configured-verified-true-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-block-icon-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-error-class-name-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-extra-class-names-auto.png create mode 100644 packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.module.css create mode 100644 packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx create mode 100644 packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.test.tsx create mode 100644 packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.tsx create mode 100644 packages/shared-components/src/message-body/DecryptionFailureBodyView/__snapshots__/DecryptionFailureBodyView.test.tsx.snap create mode 100644 packages/shared-components/src/message-body/DecryptionFailureBodyView/index.tsx delete mode 100644 res/css/views/messages/_DecryptionFailureBody.pcss delete mode 100644 src/components/views/messages/DecryptionFailureBody.tsx create mode 100644 src/viewmodels/message-body/DecryptionFailureBodyViewModel.ts delete mode 100644 test/unit-tests/components/views/messages/DecryptionFailureBody-test.tsx delete mode 100644 test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap create mode 100644 test/viewmodels/message-body/DecryptionFailureBodyViewModel-test.tsx diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/default-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..27809a16211bb7d7b28506cf268e069286d59cb4 GIT binary patch literal 6924 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%0D(&-fItzuDzjIWH-ffS}I_1mCsG;utN^=PyFoj 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 literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-extra-class-names-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx/has-extra-class-names-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..27809a16211bb7d7b28506cf268e069286d59cb4 GIT binary patch literal 6924 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 span { + /* some space between the (/) icon and text */ + display: inline-flex; + gap: var(--cpd-space-1x); + + /* Center vertically */ + align-items: center; +} + +.icon { + box-sizing: border-box; + flex: 0 0 16px; +} diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx new file mode 100644 index 0000000000..741f7420de --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import React, { type JSX } from "react"; + +import type { Meta, StoryFn } from "@storybook/react-vite"; +import { + DecryptionFailureBodyView, + DecryptionFailureReason, + type DecryptionFailureBodyViewSnapshot, +} from "./DecryptionFailureBodyView"; +import { useMockedViewModel } from "../../viewmodel/useMockedViewModel"; + +type DecryptionFailureBodyProps = DecryptionFailureBodyViewSnapshot; + +const DecryptionFailureBodyViewWrapper = ({ ...rest }: DecryptionFailureBodyProps): JSX.Element => { + const vm = useMockedViewModel(rest, {}); + + return ; +}; + +export default { + title: "MessageBody/DecryptionFailureBodyView", + component: DecryptionFailureBodyViewWrapper, + tags: ["autodocs"], + argTypes: { + decryptionFailureReason: { + options: Object.entries(DecryptionFailureReason) + .filter(([key, value]) => key === value) + .map(([key]) => key), + control: { type: "select" }, + }, + }, + args: { + decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + isLocalDeviceVerified: true, + extraClassNames: ["extra_class"], + }, +} as Meta; + +const Template: StoryFn = (args) => ( + +); + +export const Default = Template.bind({}); + +export const HasExtraClassNames = Template.bind({}); +HasExtraClassNames.args = { + decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + extraClassNames: ["extra_class_1", "extra_class_2"], +}; + +export const HasErrorClassName = Template.bind({}); +HasErrorClassName.args = { + decryptionFailureReason: DecryptionFailureReason.UNSIGNED_SENDER_DEVICE, + extraClassNames: undefined, +}; + +export const HasErrorBlockIcon = Template.bind({}); +HasErrorBlockIcon.args = { + decryptionFailureReason: DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, + extraClassNames: undefined, +}; + +export const HasBackupConfiguredVerifiedFalse = Template.bind({}); +HasBackupConfiguredVerifiedFalse.args = { + decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + isLocalDeviceVerified: false, + extraClassNames: undefined, +}; + +export const HasBackupConfiguredVerifiedTrue = Template.bind({}); +HasBackupConfiguredVerifiedTrue.args = { + decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + isLocalDeviceVerified: true, + extraClassNames: undefined, +}; diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.test.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.test.tsx new file mode 100644 index 0000000000..b886584c35 --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.test.tsx @@ -0,0 +1,149 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { composeStories } from "@storybook/react-vite"; +import { render } from "@test-utils"; +import React from "react"; +import { describe, it, expect } from "vitest"; + +import { DecryptionFailureBodyView, DecryptionFailureReason } from "./DecryptionFailureBodyView"; +import { MockViewModel } from "../../viewmodel"; +import * as stories from "./DecryptionFailureBodyView.stories"; + +const { HasExtraClassNames } = composeStories(stories); + +describe("DecryptionFailureBodyView", () => { + function customRender( + decryptionFailureReason: DecryptionFailureReason, + isLocalDeviceVerified: boolean = false, + extraClassNames: string[] | undefined = undefined, + ): ReturnType { + return render( + , + ); + } + + function customRenderWithRef(ref: React.RefObject): ReturnType { + return render( + , + ); + } + + it("Should display with extra class names", () => { + // When + const { container } = render(); + + // Then + expect(container).toMatchSnapshot(); + }); + + it.each([true, false])(`Should display "Unable to decrypt message and device verification is %s"`, (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.UNABLE_TO_DECRYPT, verified); + + // Then + expect(container).toHaveTextContent("Unable to decrypt message"); + expect(container).toMatchSnapshot(); + }); + + it.each([true, false])( + `Should display "The sender has blocked you from receiving this message and device verification is %s"`, + (verified) => { + // When + const { container } = customRender( + DecryptionFailureReason.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE, + verified, + ); + + // Then + expect(container).toHaveTextContent( + "The sender has blocked you from receiving this message because your device is unverified", + ); + expect(container).toMatchSnapshot(); + }, + ); + + it.each([true, false])( + "should handle historical messages with no key backup and device verification is %s", + (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.HISTORICAL_MESSAGE_NO_KEY_BACKUP, verified); + + // Then + expect(container).toHaveTextContent("Historical messages are not available on this device"); + expect(container).toMatchSnapshot(); + }, + ); + + it.each([true, false])( + "should handle historical messages when there is a backup and device verification is %s", + (verified) => { + // When + const { container } = customRender( + DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + verified, + ); + + // Then + expect(container).toHaveTextContent( + verified ? "Unable to decrypt" : "You need to verify this device for access to historical messages", + ); + }, + ); + + it.each([true, false])( + "should handle undecryptable pre-join messages and device verification is %s", + (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.HISTORICAL_MESSAGE_USER_NOT_JOINED, verified); + + // Then + expect(container).toHaveTextContent("You don't have access to this message"); + expect(container).toMatchSnapshot(); + }, + ); + + it.each([true, false])( + "should handle messages from users who change identities after verification and device verification is %s", + (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, verified); + + // Then + expect(container).toHaveTextContent("Sender's verified identity was reset"); + expect(container).toMatchSnapshot(); + }, + ); + + it.each([true, false])( + "should handle messages from unverified devices and device verification is %s", + (verified) => { + // When + const { container } = customRender(DecryptionFailureReason.UNSIGNED_SENDER_DEVICE, verified); + + // Then + expect(container).toHaveTextContent("Sent from an insecure device"); + expect(container).toMatchSnapshot(); + }, + ); + + it("should handle ref input", async () => { + const ref = React.createRef(); + // When + const { container } = customRenderWithRef(ref); + + // Then + expect(container).toBeInstanceOf(HTMLDivElement); + expect(container.firstChild).toHaveTextContent("Unable to decrypt message"); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.tsx new file mode 100644 index 0000000000..23b0d639d5 --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.tsx @@ -0,0 +1,176 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import classNames from "classnames"; +import React, { type JSX } from "react"; +import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { type I18nApi } from "@element-hq/element-web-module-api"; + +import { type ViewModel } from "../../viewmodel/ViewModel"; +import { useViewModel } from "../../viewmodel/useViewModel"; +import styles from "./DecryptionFailureBodyView.module.css"; +import { useI18n } from "../../utils/i18nContext"; + +/** + * A reason code for a failure to decrypt an event. + */ +export enum DecryptionFailureReason { + /** A special case of {@link MEGOLM_KEY_WITHHELD}: the sender has told us it is withholding the key, because the current device is unverified. */ + MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE = "MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE", + + /** + * Message was sent before the current device was created; there is no key backup on the server, so this + * decryption failure is expected. + */ + HISTORICAL_MESSAGE_NO_KEY_BACKUP = "HISTORICAL_MESSAGE_NO_KEY_BACKUP", + + /** + * Message was sent before the current device was created; there was a key backup on the server, but we don't + * seem to have access to the backup. (Probably we don't have the right key.) + */ + HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED = "HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED", + + /** + * Message was sent when the user was not a member of the room. + */ + HISTORICAL_MESSAGE_USER_NOT_JOINED = "HISTORICAL_MESSAGE_USER_NOT_JOINED", + + /** + * The sender's identity is not verified, but was previously verified. + */ + SENDER_IDENTITY_PREVIOUSLY_VERIFIED = "SENDER_IDENTITY_PREVIOUSLY_VERIFIED", + + /** + * The sender device is not cross-signed. This will only be used if the + * device isolation mode is set to `OnlySignedDevicesIsolationMode`. + */ + UNSIGNED_SENDER_DEVICE = "UNSIGNED_SENDER_DEVICE", + + /** + * Default message for decryption failures. + */ + UNABLE_TO_DECRYPT = "UNABLE_TO_DECRYPT", +} + +export interface DecryptionFailureBodyViewSnapshot { + /** + * The decryption failure reason of the event. + */ + decryptionFailureReason: DecryptionFailureReason; + /** + * The local device verification state. + */ + isLocalDeviceVerified?: boolean; + /** + * Extra CSS classes to apply to the component + */ + extraClassNames?: string[]; +} + +/** + * The view model for the component. + */ +export type DecryptionFailureBodyViewModel = ViewModel; + +interface DecryptionFailureBodyViewProps { + /** + * The view model for the component. + */ + vm: DecryptionFailureBodyViewModel; + /** + * React ref to attach to any React components returned + */ + ref?: React.RefObject; +} + +/** + * Resolve the localized error message for a decryption failure reason. + * + * @param i18nApi - I18n API used to translate message keys. + * @param decryptionFailureReason - Reason code for the decryption failure. + * @param isLocalDeviceVerified - Whether the local device is verified, used for certain historical cases. + */ +function getErrorMessage( + i18nApi: I18nApi, + decryptionFailureReason: DecryptionFailureReason, + isLocalDeviceVerified?: boolean, +): string | JSX.Element { + const _t = i18nApi.translate; + + switch (decryptionFailureReason) { + case DecryptionFailureReason.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: + return _t("timeline|decryption_failure|blocked"); + + case DecryptionFailureReason.HISTORICAL_MESSAGE_NO_KEY_BACKUP: + return _t("timeline|decryption_failure|historical_event_no_key_backup"); + + case DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED: + if (isLocalDeviceVerified === false) { + // The user seems to have a key backup, so prompt them to verify in the hope that doing so will + // mean we can restore from backup and we'll get the key for this message. + return _t("timeline|decryption_failure|historical_event_unverified_device"); + } + // otherwise, use the default. + break; + + case DecryptionFailureReason.HISTORICAL_MESSAGE_USER_NOT_JOINED: + // TODO: event should be hidden instead of showing this error. + // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 + return _t("timeline|decryption_failure|historical_event_user_not_joined"); + + case DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: + return ( + + + {_t("timeline|decryption_failure|sender_identity_previously_verified")} + + ); + + case DecryptionFailureReason.UNSIGNED_SENDER_DEVICE: + // TODO: event should be hidden instead of showing this error. + // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 + return ( + + + {_t("timeline|decryption_failure|sender_unsigned_device")} + + ); + } + return _t("timeline|decryption_failure|unable_to_decrypt"); +} + +/** + * Get the extra CSS class for the given decryption failure reason, when one applies. + */ +function errorClassName(decryptionFailureReason: DecryptionFailureReason): string | null { + switch (decryptionFailureReason) { + case DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: + case DecryptionFailureReason.UNSIGNED_SENDER_DEVICE: + return styles.error; + } + return null; +} + +/** + * A placeholder element for messages that could not be decrypted + * + * @example + * ```tsx + * + * ``` + */ +export function DecryptionFailureBodyView({ vm, ref }: Readonly): JSX.Element { + const i18nApi = useI18n(); + const { decryptionFailureReason, isLocalDeviceVerified, extraClassNames } = useViewModel(vm); + const classes = classNames(styles.content, errorClassName(decryptionFailureReason), extraClassNames); + + return ( +
+ {getErrorMessage(i18nApi, decryptionFailureReason, isLocalDeviceVerified)} +
+ ); +} diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/__snapshots__/DecryptionFailureBodyView.test.tsx.snap b/packages/shared-components/src/message-body/DecryptionFailureBodyView/__snapshots__/DecryptionFailureBodyView.test.tsx.snap new file mode 100644 index 0000000000..7bd5899caf --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/__snapshots__/DecryptionFailureBodyView.test.tsx.snap @@ -0,0 +1,187 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DecryptionFailureBodyView > Should display "The sender has blocked you from receiving this message and device verification is false" 1`] = ` +
+
+ The sender has blocked you from receiving this message because your device is unverified +
+
+`; + +exports[`DecryptionFailureBodyView > Should display "The sender has blocked you from receiving this message and device verification is true" 1`] = ` +
+
+ The sender has blocked you from receiving this message because your device is unverified +
+
+`; + +exports[`DecryptionFailureBodyView > Should display "Unable to decrypt message and device verification is false" 1`] = ` +
+
+ Unable to decrypt message +
+
+`; + +exports[`DecryptionFailureBodyView > Should display "Unable to decrypt message and device verification is true" 1`] = ` +
+
+ Unable to decrypt message +
+
+`; + +exports[`DecryptionFailureBodyView > Should display with extra class names 1`] = ` +
+
+ Unable to decrypt message +
+
+`; + +exports[`DecryptionFailureBodyView > should handle historical messages with no key backup and device verification is false 1`] = ` +
+
+ Historical messages are not available on this device +
+
+`; + +exports[`DecryptionFailureBodyView > should handle historical messages with no key backup and device verification is true 1`] = ` +
+
+ Historical messages are not available on this device +
+
+`; + +exports[`DecryptionFailureBodyView > should handle messages from unverified devices and device verification is false 1`] = ` +
+
+ + + + + Sent from an insecure device. + +
+
+`; + +exports[`DecryptionFailureBodyView > should handle messages from unverified devices and device verification is true 1`] = ` +
+
+ + + + + Sent from an insecure device. + +
+
+`; + +exports[`DecryptionFailureBodyView > should handle messages from users who change identities after verification and device verification is false 1`] = ` +
+
+ + + + + Sender's verified identity was reset + +
+
+`; + +exports[`DecryptionFailureBodyView > should handle messages from users who change identities after verification and device verification is true 1`] = ` +
+
+ + + + + Sender's verified identity was reset + +
+
+`; + +exports[`DecryptionFailureBodyView > should handle undecryptable pre-join messages and device verification is false 1`] = ` +
+
+ You don't have access to this message +
+
+`; + +exports[`DecryptionFailureBodyView > should handle undecryptable pre-join messages and device verification is true 1`] = ` +
+
+ You don't have access to this message +
+
+`; diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/index.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/index.tsx new file mode 100644 index 0000000000..bc533a89fd --- /dev/null +++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/index.tsx @@ -0,0 +1,13 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +export { + DecryptionFailureBodyView, + DecryptionFailureReason, + type DecryptionFailureBodyViewModel, + type DecryptionFailureBodyViewSnapshot, +} from "./DecryptionFailureBodyView"; diff --git a/res/css/_components.pcss b/res/css/_components.pcss index a93f040b6c..60bca97841 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -220,7 +220,6 @@ @import "./views/messages/_CallEvent.pcss"; @import "./views/messages/_CreateEvent.pcss"; @import "./views/messages/_DateSeparator.pcss"; -@import "./views/messages/_DecryptionFailureBody.pcss"; @import "./views/messages/_DisambiguatedProfile.pcss"; @import "./views/messages/_EventTileBubble.pcss"; @import "./views/messages/_HiddenBody.pcss"; diff --git a/res/css/views/messages/_DecryptionFailureBody.pcss b/res/css/views/messages/_DecryptionFailureBody.pcss deleted file mode 100644 index 4a4940abe3..0000000000 --- a/res/css/views/messages/_DecryptionFailureBody.pcss +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -.mx_DecryptionFailureBody { - color: $secondary-content; - font-style: italic; -} - -/* Formatting for errors due to sender trust requirement failures */ -.mx_DecryptionFailureSenderTrustRequirement > span { - /* some space between the (/) icon and text */ - display: inline-flex; - gap: var(--cpd-space-1x); - - /* Center vertically */ - align-items: center; -} diff --git a/src/components/structures/MatrixClientContextProvider.tsx b/src/components/structures/MatrixClientContextProvider.tsx index 7d555f5809..f99ad901e6 100644 --- a/src/components/structures/MatrixClientContextProvider.tsx +++ b/src/components/structures/MatrixClientContextProvider.tsx @@ -11,9 +11,9 @@ import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { CryptoEvent } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; +import { LocalDeviceVerificationStateContext } from "../../contexts/LocalDeviceVerificationStateContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { useEventEmitter } from "../../hooks/useEventEmitter"; -import { LocalDeviceVerificationStateContext } from "../../contexts/LocalDeviceVerificationStateContext"; /** * A React hook whose value is whether the local device has been "verified". diff --git a/src/components/views/messages/DecryptionFailureBody.tsx b/src/components/views/messages/DecryptionFailureBody.tsx deleted file mode 100644 index f75a7c48f8..0000000000 --- a/src/components/views/messages/DecryptionFailureBody.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022-2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import classNames from "classnames"; -import React, { type JSX, useContext } from "react"; -import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; -import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; - -import { _t } from "../../../languageHandler"; -import { type IBodyProps } from "./IBodyProps"; -import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; - -function getErrorMessage(mxEvent: MatrixEvent, isVerified: boolean | undefined): string | JSX.Element { - switch (mxEvent.decryptionFailureReason) { - case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: - return _t("timeline|decryption_failure|blocked"); - - case DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP: - return _t("timeline|decryption_failure|historical_event_no_key_backup"); - - case DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED: - if (isVerified === false) { - // The user seems to have a key backup, so prompt them to verify in the hope that doing so will - // mean we can restore from backup and we'll get the key for this message. - return _t("timeline|decryption_failure|historical_event_unverified_device"); - } - // otherwise, use the default. - break; - - case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED: - // TODO: event should be hidden instead of showing this error. - // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 - return _t("timeline|decryption_failure|historical_event_user_not_joined"); - - case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: - return ( - - - {_t("timeline|decryption_failure|sender_identity_previously_verified")} - - ); - - case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: - // TODO: event should be hidden instead of showing this error. - // To be revisited as part of https://github.com/element-hq/element-meta/issues/2449 - return ( - - - {_t("timeline|decryption_failure|sender_unsigned_device")} - - ); - } - return _t("timeline|decryption_failure|unable_to_decrypt"); -} - -/** Get an extra CSS class, specific to the decryption failure reason */ -function errorClassName(mxEvent: MatrixEvent): string | null { - switch (mxEvent.decryptionFailureReason) { - case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: - case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: - return "mx_DecryptionFailureSenderTrustRequirement"; - - default: - return null; - } -} - -// A placeholder element for messages that could not be decrypted -export const DecryptionFailureBody = ({ mxEvent, ref }: IBodyProps): JSX.Element => { - const verificationState = useContext(LocalDeviceVerificationStateContext); - const classes = classNames("mx_DecryptionFailureBody", "mx_EventTile_content", errorClassName(mxEvent)); - - return ( -
- {getErrorMessage(mxEvent, verificationState)} -
- ); -}; diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index 6d124c88a8..6bb28fd188 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import mime from "mime"; -import React, { createRef } from "react"; +import React, { type JSX, createRef, useContext, useEffect } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { EventType, @@ -18,7 +18,9 @@ import { M_POLL_START, type IContent, } from "matrix-js-sdk/src/matrix"; +import { useCreateAutoDisposedViewModel, DecryptionFailureBodyView } from "@element-hq/web-shared-components"; +import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; import SettingsStore from "../../../settings/SettingsStore"; import { Mjolnir } from "../../../mjolnir/Mjolnir"; import RedactedBody from "./RedactedBody"; @@ -36,8 +38,8 @@ import MPollBody from "./MPollBody"; import MLocationBody from "./MLocationBody"; import MjolnirBody from "./MjolnirBody"; import MBeaconBody from "./MBeaconBody"; -import { DecryptionFailureBody } from "./DecryptionFailureBody"; import { type GetRelationsForEvent, type IEventTileOps } from "../rooms/EventTile"; +import { DecryptionFailureBodyViewModel } from "../../../viewmodels/message-body/DecryptionFailureBodyViewModel"; // onMessageAllowed is handled internally interface IProps extends Omit { @@ -248,7 +250,7 @@ export default class MessageEvent extends React.Component implements IMe if (!this.props.mxEvent.isRedacted()) { // only resolve BodyType if event is not redacted if (this.props.mxEvent.isDecryptionFailure()) { - BodyType = DecryptionFailureBody; + BodyType = DecryptionFailureBodyWrapper; } else if (type && this.evTypes.has(type)) { BodyType = this.evTypes.get(type)!; } else if (msgtype && this.bodyTypes.has(msgtype)) { @@ -328,3 +330,22 @@ const CaptionBody: React.FunctionComponent ); + +/** + * Bridge decryption-failure events into the view model using current local verification state. + * This wrapper can be removed after MessageEvent has been changed to a function component. + */ +function DecryptionFailureBodyWrapper({ mxEvent, ref }: IBodyProps): JSX.Element { + const verificationState = useContext(LocalDeviceVerificationStateContext); + const vm = useCreateAutoDisposedViewModel( + () => + new DecryptionFailureBodyViewModel({ + decryptionFailureCode: mxEvent.decryptionFailureReason, + verificationState, + }), + ); + useEffect(() => { + vm.setVerificationState(verificationState); + }, [verificationState, vm]); + return ; +} diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 2546936ab5..93225a43dc 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { createRef, type JSX, type Ref, type MouseEvent, type ReactNode } from "react"; +import React, { createRef, useContext, useEffect, type JSX, type Ref, type MouseEvent, type ReactNode } from "react"; import classNames from "classnames"; import { EventStatus, @@ -36,13 +36,14 @@ import { import { Tooltip } from "@vector-im/compound-web"; import { uniqueId } from "lodash"; import { CircleIcon, CheckCircleIcon, ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { useCreateAutoDisposedViewModel, DecryptionFailureBodyView } from "@element-hq/web-shared-components"; +import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext"; import ReplyChain from "../elements/ReplyChain"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; import { Layout } from "../../../settings/enums/Layout"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { DecryptionFailureBody } from "../messages/DecryptionFailureBody"; import RoomAvatar from "../avatars/RoomAvatar"; import MessageContextMenu from "../context_menus/MessageContextMenu"; import { aboveRightOf } from "../../structures/ContextMenu"; @@ -84,6 +85,7 @@ import PinningUtils from "../../../utils/PinningUtils"; import { PinnedMessageBadge } from "../messages/PinnedMessageBadge"; import { EventPreview } from "./EventPreview"; import { ElementCallEventType } from "../../../call-types"; +import { DecryptionFailureBodyViewModel } from "../../../viewmodels/message-body/DecryptionFailureBodyViewModel"; import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx"; import { E2ePadlock, E2ePadlockIcon } from "./EventTile/E2ePadlock.tsx"; @@ -1373,7 +1375,7 @@ export class UnwrappedEventTile extends React.Component {this.props.mxEvent.isRedacted() ? ( ) : this.props.mxEvent.isDecryptionFailure() ? ( - + ) : ( )} @@ -1569,3 +1571,23 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element { ); } + +/** + * Bridge decryption-failure events into the view model using current local verification state. + * This wrapper can be removed after EventTile has been changed to a function component. + */ +function DecryptionFailureBodyWrapper({ mxEvent }: { mxEvent: MatrixEvent }): JSX.Element { + const verificationState = useContext(LocalDeviceVerificationStateContext); + const vm = useCreateAutoDisposedViewModel( + () => + new DecryptionFailureBodyViewModel({ + decryptionFailureCode: mxEvent.decryptionFailureReason, + verificationState, + }), + ); + useEffect(() => { + vm.setVerificationState(verificationState); + }, [verificationState, vm]); + + return ; +} diff --git a/src/contexts/LocalDeviceVerificationStateContext.ts b/src/contexts/LocalDeviceVerificationStateContext.ts index df5af67252..9d0a24ede0 100644 --- a/src/contexts/LocalDeviceVerificationStateContext.ts +++ b/src/contexts/LocalDeviceVerificationStateContext.ts @@ -1,10 +1,9 @@ /* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ import { createContext } from "react"; @@ -13,7 +12,5 @@ import { createContext } from "react"; * * (Specifically, this is true if we have done enough verification to confirm that the published public cross-signing * keys are genuine -- which normally means that we or another device will have published a signature of this device.) - * - * This context is available to all components under {@link LoggedInView}, via {@link MatrixClientContextProvider}. */ export const LocalDeviceVerificationStateContext = createContext(false); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c9e5e1e028..a5b5ec50ca 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3394,12 +3394,7 @@ "creation_summary_dm": "%(creator)s created this DM.", "creation_summary_room": "%(creator)s created and configured the room.", "decryption_failure": { - "blocked": "The sender has blocked you from receiving this message because your device is unverified", - "historical_event_no_key_backup": "Historical messages are not available on this device", - "historical_event_unverified_device": "You need to verify this device for access to historical messages", - "historical_event_user_not_joined": "You don't have access to this message", "sender_identity_previously_verified": "Sender's verified identity was reset", - "sender_unsigned_device": "Sent from an insecure device.", "unable_to_decrypt": "Unable to decrypt message" }, "disambiguated_profile": "%(displayName)s (%(matrixId)s)", diff --git a/src/viewmodels/message-body/DecryptionFailureBodyViewModel.ts b/src/viewmodels/message-body/DecryptionFailureBodyViewModel.ts new file mode 100644 index 0000000000..965c4a0942 --- /dev/null +++ b/src/viewmodels/message-body/DecryptionFailureBodyViewModel.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; +import { + BaseViewModel, + DecryptionFailureReason, + type DecryptionFailureBodyViewSnapshot as DecryptionFailureBodyViewSnapshotInterface, + type DecryptionFailureBodyViewModel as DecryptionFailureBodyViewModelInterface, +} from "@element-hq/web-shared-components"; + +export interface DecryptionFailureBodyViewModelProps { + /** + * The message event being rendered. + */ + decryptionFailureCode: DecryptionFailureCode | null; + /** + * The local device verification state. + */ + verificationState?: boolean; + /** + * Extra CSS classes to apply to the component + */ + extraClassNames?: string[]; +} + +/** + * ViewModel for the decryption failure body, providing the current state of the component. + */ +export class DecryptionFailureBodyViewModel + extends BaseViewModel + implements DecryptionFailureBodyViewModelInterface +{ + /** + * Convert enum DecryptionFailureCode to enum DecryptionFailureReason. + */ + private static getDecryptionReasonFromCode( + decryptionFailureCode: DecryptionFailureCode | null, + ): DecryptionFailureReason { + switch (decryptionFailureCode) { + case DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED: + return DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED; + case DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP: + return DecryptionFailureReason.HISTORICAL_MESSAGE_NO_KEY_BACKUP; + case DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED: + return DecryptionFailureReason.HISTORICAL_MESSAGE_USER_NOT_JOINED; + case DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE: + return DecryptionFailureReason.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE; + case DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED: + return DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED; + case DecryptionFailureCode.UNSIGNED_SENDER_DEVICE: + return DecryptionFailureReason.UNSIGNED_SENDER_DEVICE; + default: + return DecryptionFailureReason.UNABLE_TO_DECRYPT; + } + } + + /** + * @param decryptionFailureCode - The decryption failure code for the event. + * @param verificationState - The local device verification state. + * @param extraClassNames - Extra CSS classes to apply to the component. + */ + private static readonly computeSnapshot = ( + decryptionFailureCode: DecryptionFailureCode | null, + verificationState?: boolean, + extraClassNames?: string[], + ): DecryptionFailureBodyViewSnapshotInterface => { + // Keep mx_DecryptionFailureBody and mx_EventTile_content to support the compatibility with existing timeline and the all the layout + const defaultClassNames = ["mx_DecryptionFailureBody", "mx_EventTile_content"]; + return { + decryptionFailureReason: DecryptionFailureBodyViewModel.getDecryptionReasonFromCode(decryptionFailureCode), + isLocalDeviceVerified: verificationState, + extraClassNames: extraClassNames ? defaultClassNames.concat(extraClassNames) : defaultClassNames, + }; + }; + + public constructor(props: DecryptionFailureBodyViewModelProps) { + super( + props, + DecryptionFailureBodyViewModel.computeSnapshot( + props.decryptionFailureCode, + props.verificationState, + props.extraClassNames, + ), + ); + } + + /** + * Updates the properties of the view model and recomputes the snapshot. + * @param verificationState - The updated local device verification state. + */ + public setVerificationState(verificationState?: boolean): void { + this.props.verificationState = verificationState; + this.snapshot.merge({ isLocalDeviceVerified: verificationState }); + } +} diff --git a/test/unit-tests/components/structures/MatrixClientContextProvider-test.tsx b/test/unit-tests/components/structures/MatrixClientContextProvider-test.tsx index 2710dcd57a..b70017ee4f 100644 --- a/test/unit-tests/components/structures/MatrixClientContextProvider-test.tsx +++ b/test/unit-tests/components/structures/MatrixClientContextProvider-test.tsx @@ -11,9 +11,9 @@ import React, { useContext } from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; +import { LocalDeviceVerificationStateContext } from "../../../../src/contexts/LocalDeviceVerificationStateContext"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { MatrixClientContextProvider } from "../../../../src/components/structures/MatrixClientContextProvider"; -import { LocalDeviceVerificationStateContext } from "../../../../src/contexts/LocalDeviceVerificationStateContext"; import { flushPromises, getMockClientWithEventEmitter, diff --git a/test/unit-tests/components/views/messages/DecryptionFailureBody-test.tsx b/test/unit-tests/components/views/messages/DecryptionFailureBody-test.tsx deleted file mode 100644 index ce628da309..0000000000 --- a/test/unit-tests/components/views/messages/DecryptionFailureBody-test.tsx +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * Copyright 2023 The Matrix.org Foundation C.I.C. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import React from "react"; -import { render } from "jest-matrix-react"; -import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing"; -import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; - -import { mkEvent } from "../../../../test-utils"; -import { DecryptionFailureBody } from "../../../../../src/components/views/messages/DecryptionFailureBody"; -import { LocalDeviceVerificationStateContext } from "../../../../../src/contexts/LocalDeviceVerificationStateContext"; - -describe("DecryptionFailureBody", () => { - function customRender(event: MatrixEvent, localDeviceVerified: boolean = false) { - return render( - - - , - ); - } - - it(`Should display "Unable to decrypt message"`, () => { - // When - const event = mkEvent({ - type: "m.room.message", - room: "myfakeroom", - user: "myfakeuser", - content: { - msgtype: "m.bad.encrypted", - }, - event: true, - }); - const { container } = customRender(event); - - // Then - expect(container).toMatchSnapshot(); - }); - - it(`Should display "The sender has blocked you from receiving this message"`, async () => { - // When - const event = await mkDecryptionFailureMatrixEvent({ - code: DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE, - msg: "withheld", - roomId: "myfakeroom", - sender: "myfakeuser", - }); - - const { container } = customRender(event); - - // Then - expect(container).toMatchSnapshot(); - }); - - it("should handle historical messages with no key backup", async () => { - // When - const event = await mkDecryptionFailureMatrixEvent({ - code: DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP, - msg: "No backup", - roomId: "fakeroom", - sender: "fakesender", - }); - const { container } = customRender(event); - - // Then - expect(container).toHaveTextContent("Historical messages are not available on this device"); - }); - - it.each([true, false])( - "should handle historical messages when there is a backup and device verification is %s", - async (verified) => { - // When - const event = await mkDecryptionFailureMatrixEvent({ - code: DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, - msg: "Failure", - roomId: "fakeroom", - sender: "fakesender", - }); - const { container } = customRender(event, verified); - - // Then - expect(container).toHaveTextContent( - verified ? "Unable to decrypt" : "You need to verify this device for access to historical messages", - ); - }, - ); - - it("should handle undecryptable pre-join messages", async () => { - // When - const event = await mkDecryptionFailureMatrixEvent({ - code: DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED, - msg: "Not joined", - roomId: "fakeroom", - sender: "fakesender", - }); - const { container } = customRender(event); - - // Then - expect(container).toHaveTextContent("You don't have access to this message"); - }); - - it("should handle messages from users who change identities after verification", async () => { - // When - const event = await mkDecryptionFailureMatrixEvent({ - code: DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, - msg: "User previously verified", - roomId: "fakeroom", - sender: "fakesender", - }); - const { container } = customRender(event); - - // Then - expect(container).toMatchSnapshot(); - }); - - it("should handle messages from unverified devices", async () => { - // When - const event = await mkDecryptionFailureMatrixEvent({ - code: DecryptionFailureCode.UNSIGNED_SENDER_DEVICE, - msg: "Unsigned device", - roomId: "fakeroom", - sender: "fakesender", - }); - const { container } = customRender(event); - - // Then - expect(container).toHaveTextContent("Sent from an insecure device"); - }); -}); diff --git a/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap deleted file mode 100644 index 823b1f5e6c..0000000000 --- a/test/unit-tests/components/views/messages/__snapshots__/DecryptionFailureBody-test.tsx.snap +++ /dev/null @@ -1,45 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`DecryptionFailureBody Should display "The sender has blocked you from receiving this message" 1`] = ` -
-
- The sender has blocked you from receiving this message because your device is unverified -
-
-`; - -exports[`DecryptionFailureBody Should display "Unable to decrypt message" 1`] = ` -
-
- Unable to decrypt message -
-
-`; - -exports[`DecryptionFailureBody should handle messages from users who change identities after verification 1`] = ` -
-
- - - - - Sender's verified identity was reset - -
-
-`; diff --git a/test/viewmodels/message-body/DecryptionFailureBodyViewModel-test.tsx b/test/viewmodels/message-body/DecryptionFailureBodyViewModel-test.tsx new file mode 100644 index 0000000000..85580842de --- /dev/null +++ b/test/viewmodels/message-body/DecryptionFailureBodyViewModel-test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api"; +import { DecryptionFailureReason } from "@element-hq/web-shared-components"; + +import { DecryptionFailureBodyViewModel } from "../../../src/viewmodels/message-body/DecryptionFailureBodyViewModel"; + +describe("DecryptionFailureBodyViewModel", () => { + it("should return the snapshot", () => { + const vm = new DecryptionFailureBodyViewModel({ + decryptionFailureCode: null, + verificationState: true, + }); + expect(vm.getSnapshot()).toMatchObject({ + decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + isLocalDeviceVerified: true, + }); + }); + + it("should return the snapshot with extra class names", () => { + const vm = new DecryptionFailureBodyViewModel({ + decryptionFailureCode: null, + verificationState: true, + extraClassNames: ["custom-class"], + }); + expect(vm.getSnapshot()).toMatchObject({ + decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + isLocalDeviceVerified: true, + extraClassNames: ["mx_DecryptionFailureBody", "mx_EventTile_content", "custom-class"], + }); + }); + + it.each([ + { + code: DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + reason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED, + }, + { + code: DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP, + reason: DecryptionFailureReason.HISTORICAL_MESSAGE_NO_KEY_BACKUP, + }, + { + code: DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED, + reason: DecryptionFailureReason.HISTORICAL_MESSAGE_USER_NOT_JOINED, + }, + { + code: DecryptionFailureCode.MEGOLM_KEY_WITHHELD, + reason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + }, + { + code: DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE, + reason: DecryptionFailureReason.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE, + }, + { + code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, + reason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + }, + { + code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX, + reason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + }, + { + code: DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, + reason: DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED, + }, + { + code: DecryptionFailureCode.UNKNOWN_ERROR, + reason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + }, + { + code: DecryptionFailureCode.UNKNOWN_SENDER_DEVICE, + reason: DecryptionFailureReason.UNABLE_TO_DECRYPT, + }, + { + code: DecryptionFailureCode.UNSIGNED_SENDER_DEVICE, + reason: DecryptionFailureReason.UNSIGNED_SENDER_DEVICE, + }, + ])("should return the snapshot with code converted to reason (%s)", ({ code, reason }) => { + const vm = new DecryptionFailureBodyViewModel({ + decryptionFailureCode: code, + }); + + expect(vm.getSnapshot().decryptionFailureReason).toBe(reason); + }); + + it("should update snapshot when setProps is called with new verificationState", () => { + const vm = new DecryptionFailureBodyViewModel({ + decryptionFailureCode: DecryptionFailureCode.UNKNOWN_ERROR, + verificationState: false, + }); + expect(vm.getSnapshot().isLocalDeviceVerified).toBe(false); + + vm.setVerificationState(true); + expect(vm.getSnapshot().isLocalDeviceVerified).toBe(true); + }); +});