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 <florianduros@element.io>
Co-authored-by: Zack <zazi21@student.bth.se>
This commit is contained in:
rbondesson
2026-01-30 12:44:23 +00:00
committed by GitHub
co-authored by Florian Duros Zack
parent 62c7fe5408
commit 25d24d478f
27 changed files with 899 additions and 308 deletions
@@ -80,6 +80,15 @@
"n_minutes_ago": "%(num)s minutes ago"
},
"timeline": {
"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"
},
"m.audio": {
"audio_player": "Audio player",
"error_downloading_audio": "Error downloading audio",
+1 -1
View File
@@ -15,6 +15,7 @@ export * from "./composer/Banner";
export * from "./crypto/SasEmoji";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./message-body/DecryptionFailureBodyView";
export * from "./message-body/ReactionsRowButtonTooltip";
export * from "./pill-input/Pill";
export * from "./pill-input/PillInput";
@@ -36,6 +37,5 @@ export * from "./utils/DateUtils";
export * from "./utils/numbers";
export * from "./utils/FormattingUtils";
export * from "./utils/I18nApi";
// MVVM
export * from "./viewmodel";
@@ -0,0 +1,26 @@
/*
* 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.
*/
.content {
color: var(--cpd-color-text-secondary);
font-style: italic;
}
/* Formatting for errors due to sender trust requirement failures */
.error > 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;
}
@@ -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 <DecryptionFailureBodyView vm={vm} />;
};
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<typeof DecryptionFailureBodyViewWrapper>;
const Template: StoryFn<typeof DecryptionFailureBodyViewWrapper> = (args) => (
<DecryptionFailureBodyViewWrapper {...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,
};
@@ -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<typeof render> {
return render(
<DecryptionFailureBodyView
vm={new MockViewModel({ decryptionFailureReason, isLocalDeviceVerified, extraClassNames })}
/>,
);
}
function customRenderWithRef(ref: React.RefObject<any>): ReturnType<typeof render> {
return render(
<DecryptionFailureBodyView
vm={new MockViewModel({ decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT })}
ref={ref}
/>,
);
}
it("Should display with extra class names", () => {
// When
const { container } = render(<HasExtraClassNames />);
// 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<HTMLDivElement>();
// When
const { container } = customRenderWithRef(ref);
// Then
expect(container).toBeInstanceOf(HTMLDivElement);
expect(container.firstChild).toHaveTextContent("Unable to decrypt message");
expect(ref.current).toBeInstanceOf(HTMLDivElement);
});
});
@@ -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<DecryptionFailureBodyViewSnapshot>;
interface DecryptionFailureBodyViewProps {
/**
* The view model for the component.
*/
vm: DecryptionFailureBodyViewModel;
/**
* React ref to attach to any React components returned
*/
ref?: React.RefObject<any>;
}
/**
* 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 (
<span>
<BlockIcon className={styles.icon} width="16px" height="16px" />
{_t("timeline|decryption_failure|sender_identity_previously_verified")}
</span>
);
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 (
<span>
<BlockIcon className={styles.icon} width="16px" height="16px" />
{_t("timeline|decryption_failure|sender_unsigned_device")}
</span>
);
}
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
* <DecryptionFailureBodyView vm={DecryptionFailureBodyViewModel} />
* ```
*/
export function DecryptionFailureBodyView({ vm, ref }: Readonly<DecryptionFailureBodyViewProps>): JSX.Element {
const i18nApi = useI18n();
const { decryptionFailureReason, isLocalDeviceVerified, extraClassNames } = useViewModel(vm);
const classes = classNames(styles.content, errorClassName(decryptionFailureReason), extraClassNames);
return (
<div className={classes} ref={ref}>
{getErrorMessage(i18nApi, decryptionFailureReason, isLocalDeviceVerified)}
</div>
);
}
@@ -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`] = `
<div>
<div
class="content"
>
The sender has blocked you from receiving this message because your device is unverified
</div>
</div>
`;
exports[`DecryptionFailureBodyView > Should display "The sender has blocked you from receiving this message and device verification is true" 1`] = `
<div>
<div
class="content"
>
The sender has blocked you from receiving this message because your device is unverified
</div>
</div>
`;
exports[`DecryptionFailureBodyView > Should display "Unable to decrypt message and device verification is false" 1`] = `
<div>
<div
class="content"
>
Unable to decrypt message
</div>
</div>
`;
exports[`DecryptionFailureBodyView > Should display "Unable to decrypt message and device verification is true" 1`] = `
<div>
<div
class="content"
>
Unable to decrypt message
</div>
</div>
`;
exports[`DecryptionFailureBodyView > Should display with extra class names 1`] = `
<div>
<div
class="content extra_class_1 extra_class_2"
>
Unable to decrypt message
</div>
</div>
`;
exports[`DecryptionFailureBodyView > should handle historical messages with no key backup and device verification is false 1`] = `
<div>
<div
class="content"
>
Historical messages are not available on this device
</div>
</div>
`;
exports[`DecryptionFailureBodyView > should handle historical messages with no key backup and device verification is true 1`] = `
<div>
<div
class="content"
>
Historical messages are not available on this device
</div>
</div>
`;
exports[`DecryptionFailureBodyView > should handle messages from unverified devices and device verification is false 1`] = `
<div>
<div
class="content error"
>
<span>
<svg
class="icon"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12q0-1.35-.437-2.6A8 8 0 0 0 18.3 7.1L7.1 18.3q1.05.825 2.3 1.262T12 20m-6.3-3.1L16.9 5.7a8 8 0 0 0-2.3-1.263A7.8 7.8 0 0 0 12 4Q8.65 4 6.325 6.325T4 12q0 1.35.438 2.6A8 8 0 0 0 5.7 16.9"
/>
</svg>
Sent from an insecure device.
</span>
</div>
</div>
`;
exports[`DecryptionFailureBodyView > should handle messages from unverified devices and device verification is true 1`] = `
<div>
<div
class="content error"
>
<span>
<svg
class="icon"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12q0-1.35-.437-2.6A8 8 0 0 0 18.3 7.1L7.1 18.3q1.05.825 2.3 1.262T12 20m-6.3-3.1L16.9 5.7a8 8 0 0 0-2.3-1.263A7.8 7.8 0 0 0 12 4Q8.65 4 6.325 6.325T4 12q0 1.35.438 2.6A8 8 0 0 0 5.7 16.9"
/>
</svg>
Sent from an insecure device.
</span>
</div>
</div>
`;
exports[`DecryptionFailureBodyView > should handle messages from users who change identities after verification and device verification is false 1`] = `
<div>
<div
class="content error"
>
<span>
<svg
class="icon"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12q0-1.35-.437-2.6A8 8 0 0 0 18.3 7.1L7.1 18.3q1.05.825 2.3 1.262T12 20m-6.3-3.1L16.9 5.7a8 8 0 0 0-2.3-1.263A7.8 7.8 0 0 0 12 4Q8.65 4 6.325 6.325T4 12q0 1.35.438 2.6A8 8 0 0 0 5.7 16.9"
/>
</svg>
Sender's verified identity was reset
</span>
</div>
</div>
`;
exports[`DecryptionFailureBodyView > should handle messages from users who change identities after verification and device verification is true 1`] = `
<div>
<div
class="content error"
>
<span>
<svg
class="icon"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 22a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12q0-1.35-.437-2.6A8 8 0 0 0 18.3 7.1L7.1 18.3q1.05.825 2.3 1.262T12 20m-6.3-3.1L16.9 5.7a8 8 0 0 0-2.3-1.263A7.8 7.8 0 0 0 12 4Q8.65 4 6.325 6.325T4 12q0 1.35.438 2.6A8 8 0 0 0 5.7 16.9"
/>
</svg>
Sender's verified identity was reset
</span>
</div>
</div>
`;
exports[`DecryptionFailureBodyView > should handle undecryptable pre-join messages and device verification is false 1`] = `
<div>
<div
class="content"
>
You don't have access to this message
</div>
</div>
`;
exports[`DecryptionFailureBodyView > should handle undecryptable pre-join messages and device verification is true 1`] = `
<div>
<div
class="content"
>
You don't have access to this message
</div>
</div>
`;
@@ -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";