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:
co-authored by
Florian Duros
Zack
parent
62c7fe5408
commit
25d24d478f
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@@ -80,6 +80,15 @@
|
|||||||
"n_minutes_ago": "%(num)s minutes ago"
|
"n_minutes_ago": "%(num)s minutes ago"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"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": {
|
"m.audio": {
|
||||||
"audio_player": "Audio player",
|
"audio_player": "Audio player",
|
||||||
"error_downloading_audio": "Error downloading audio",
|
"error_downloading_audio": "Error downloading audio",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export * from "./composer/Banner";
|
|||||||
export * from "./crypto/SasEmoji";
|
export * from "./crypto/SasEmoji";
|
||||||
export * from "./event-tiles/TextualEventView";
|
export * from "./event-tiles/TextualEventView";
|
||||||
export * from "./message-body/MediaBody";
|
export * from "./message-body/MediaBody";
|
||||||
|
export * from "./message-body/DecryptionFailureBodyView";
|
||||||
export * from "./message-body/ReactionsRowButtonTooltip";
|
export * from "./message-body/ReactionsRowButtonTooltip";
|
||||||
export * from "./pill-input/Pill";
|
export * from "./pill-input/Pill";
|
||||||
export * from "./pill-input/PillInput";
|
export * from "./pill-input/PillInput";
|
||||||
@@ -36,6 +37,5 @@ export * from "./utils/DateUtils";
|
|||||||
export * from "./utils/numbers";
|
export * from "./utils/numbers";
|
||||||
export * from "./utils/FormattingUtils";
|
export * from "./utils/FormattingUtils";
|
||||||
export * from "./utils/I18nApi";
|
export * from "./utils/I18nApi";
|
||||||
|
|
||||||
// MVVM
|
// MVVM
|
||||||
export * from "./viewmodel";
|
export * from "./viewmodel";
|
||||||
|
|||||||
+26
@@ -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;
|
||||||
|
}
|
||||||
+81
@@ -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,
|
||||||
|
};
|
||||||
+149
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+176
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+187
@@ -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";
|
||||||
@@ -220,7 +220,6 @@
|
|||||||
@import "./views/messages/_CallEvent.pcss";
|
@import "./views/messages/_CallEvent.pcss";
|
||||||
@import "./views/messages/_CreateEvent.pcss";
|
@import "./views/messages/_CreateEvent.pcss";
|
||||||
@import "./views/messages/_DateSeparator.pcss";
|
@import "./views/messages/_DateSeparator.pcss";
|
||||||
@import "./views/messages/_DecryptionFailureBody.pcss";
|
|
||||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||||
@import "./views/messages/_EventTileBubble.pcss";
|
@import "./views/messages/_EventTileBubble.pcss";
|
||||||
@import "./views/messages/_HiddenBody.pcss";
|
@import "./views/messages/_HiddenBody.pcss";
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -11,9 +11,9 @@ import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
|||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { LocalDeviceVerificationStateContext } from "../../contexts/LocalDeviceVerificationStateContext";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { useEventEmitter } from "../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../hooks/useEventEmitter";
|
||||||
import { LocalDeviceVerificationStateContext } from "../../contexts/LocalDeviceVerificationStateContext";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A React hook whose value is whether the local device has been "verified".
|
* A React hook whose value is whether the local device has been "verified".
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<span>
|
|
||||||
<BlockIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
{_t("timeline|decryption_failure|sender_identity_previously_verified")}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<span>
|
|
||||||
<BlockIcon className="mx_Icon mx_Icon_16" />
|
|
||||||
{_t("timeline|decryption_failure|sender_unsigned_device")}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 (
|
|
||||||
<div className={classes} ref={ref}>
|
|
||||||
{getErrorMessage(mxEvent, verificationState)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import mime from "mime";
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
import {
|
import {
|
||||||
EventType,
|
EventType,
|
||||||
@@ -18,7 +18,9 @@ import {
|
|||||||
M_POLL_START,
|
M_POLL_START,
|
||||||
type IContent,
|
type IContent,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} 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 SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { Mjolnir } from "../../../mjolnir/Mjolnir";
|
import { Mjolnir } from "../../../mjolnir/Mjolnir";
|
||||||
import RedactedBody from "./RedactedBody";
|
import RedactedBody from "./RedactedBody";
|
||||||
@@ -36,8 +38,8 @@ import MPollBody from "./MPollBody";
|
|||||||
import MLocationBody from "./MLocationBody";
|
import MLocationBody from "./MLocationBody";
|
||||||
import MjolnirBody from "./MjolnirBody";
|
import MjolnirBody from "./MjolnirBody";
|
||||||
import MBeaconBody from "./MBeaconBody";
|
import MBeaconBody from "./MBeaconBody";
|
||||||
import { DecryptionFailureBody } from "./DecryptionFailureBody";
|
|
||||||
import { type GetRelationsForEvent, type IEventTileOps } from "../rooms/EventTile";
|
import { type GetRelationsForEvent, type IEventTileOps } from "../rooms/EventTile";
|
||||||
|
import { DecryptionFailureBodyViewModel } from "../../../viewmodels/message-body/DecryptionFailureBodyViewModel";
|
||||||
|
|
||||||
// onMessageAllowed is handled internally
|
// onMessageAllowed is handled internally
|
||||||
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
||||||
@@ -248,7 +250,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||||||
if (!this.props.mxEvent.isRedacted()) {
|
if (!this.props.mxEvent.isRedacted()) {
|
||||||
// only resolve BodyType if event is not redacted
|
// only resolve BodyType if event is not redacted
|
||||||
if (this.props.mxEvent.isDecryptionFailure()) {
|
if (this.props.mxEvent.isDecryptionFailure()) {
|
||||||
BodyType = DecryptionFailureBody;
|
BodyType = DecryptionFailureBodyWrapper;
|
||||||
} else if (type && this.evTypes.has(type)) {
|
} else if (type && this.evTypes.has(type)) {
|
||||||
BodyType = this.evTypes.get(type)!;
|
BodyType = this.evTypes.get(type)!;
|
||||||
} else if (msgtype && this.bodyTypes.has(msgtype)) {
|
} else if (msgtype && this.bodyTypes.has(msgtype)) {
|
||||||
@@ -328,3 +330,22 @@ const CaptionBody: React.FunctionComponent<IBodyProps & { WrappedBodyType: React
|
|||||||
<TextualBody {...{ ...props, ref: undefined }} />
|
<TextualBody {...{ ...props, ref: undefined }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <DecryptionFailureBodyView vm={vm} ref={ref} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
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 classNames from "classnames";
|
||||||
import {
|
import {
|
||||||
EventStatus,
|
EventStatus,
|
||||||
@@ -36,13 +36,14 @@ import {
|
|||||||
import { Tooltip } from "@vector-im/compound-web";
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
import { uniqueId } from "lodash";
|
import { uniqueId } from "lodash";
|
||||||
import { CircleIcon, CheckCircleIcon, ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
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 ReplyChain from "../elements/ReplyChain";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { Layout } from "../../../settings/enums/Layout";
|
import { Layout } from "../../../settings/enums/Layout";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { DecryptionFailureBody } from "../messages/DecryptionFailureBody";
|
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||||
import { aboveRightOf } from "../../structures/ContextMenu";
|
import { aboveRightOf } from "../../structures/ContextMenu";
|
||||||
@@ -84,6 +85,7 @@ import PinningUtils from "../../../utils/PinningUtils";
|
|||||||
import { PinnedMessageBadge } from "../messages/PinnedMessageBadge";
|
import { PinnedMessageBadge } from "../messages/PinnedMessageBadge";
|
||||||
import { EventPreview } from "./EventPreview";
|
import { EventPreview } from "./EventPreview";
|
||||||
import { ElementCallEventType } from "../../../call-types";
|
import { ElementCallEventType } from "../../../call-types";
|
||||||
|
import { DecryptionFailureBodyViewModel } from "../../../viewmodels/message-body/DecryptionFailureBodyViewModel";
|
||||||
import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx";
|
import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx";
|
||||||
import { E2ePadlock, E2ePadlockIcon } from "./EventTile/E2ePadlock.tsx";
|
import { E2ePadlock, E2ePadlockIcon } from "./EventTile/E2ePadlock.tsx";
|
||||||
|
|
||||||
@@ -1373,7 +1375,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
{this.props.mxEvent.isRedacted() ? (
|
{this.props.mxEvent.isRedacted() ? (
|
||||||
<RedactedBody mxEvent={this.props.mxEvent} />
|
<RedactedBody mxEvent={this.props.mxEvent} />
|
||||||
) : this.props.mxEvent.isDecryptionFailure() ? (
|
) : this.props.mxEvent.isDecryptionFailure() ? (
|
||||||
<DecryptionFailureBody mxEvent={this.props.mxEvent} />
|
<DecryptionFailureBodyWrapper mxEvent={this.props.mxEvent} />
|
||||||
) : (
|
) : (
|
||||||
<EventPreview mxEvent={this.props.mxEvent} />
|
<EventPreview mxEvent={this.props.mxEvent} />
|
||||||
)}
|
)}
|
||||||
@@ -1569,3 +1571,23 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <DecryptionFailureBodyView vm={vm} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
* Copyright 2026 Element Creations 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
|
||||||
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.
|
||||||
Please see LICENSE files in the repository root for full details.
|
*/
|
||||||
*/
|
|
||||||
|
|
||||||
import { createContext } from "react";
|
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
|
* (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.)
|
* 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);
|
export const LocalDeviceVerificationStateContext = createContext(false);
|
||||||
|
|||||||
@@ -3394,12 +3394,7 @@
|
|||||||
"creation_summary_dm": "%(creator)s created this DM.",
|
"creation_summary_dm": "%(creator)s created this DM.",
|
||||||
"creation_summary_room": "%(creator)s created and configured the room.",
|
"creation_summary_room": "%(creator)s created and configured the room.",
|
||||||
"decryption_failure": {
|
"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_identity_previously_verified": "Sender's verified identity was reset",
|
||||||
"sender_unsigned_device": "Sent from an insecure device.",
|
|
||||||
"unable_to_decrypt": "Unable to decrypt message"
|
"unable_to_decrypt": "Unable to decrypt message"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
|
|||||||
@@ -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<DecryptionFailureBodyViewSnapshotInterface, DecryptionFailureBodyViewModelProps>
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,9 @@ import React, { useContext } from "react";
|
|||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
import { CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
|
import { LocalDeviceVerificationStateContext } from "../../../../src/contexts/LocalDeviceVerificationStateContext";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
import { MatrixClientContextProvider } from "../../../../src/components/structures/MatrixClientContextProvider";
|
import { MatrixClientContextProvider } from "../../../../src/components/structures/MatrixClientContextProvider";
|
||||||
import { LocalDeviceVerificationStateContext } from "../../../../src/contexts/LocalDeviceVerificationStateContext";
|
|
||||||
import {
|
import {
|
||||||
flushPromises,
|
flushPromises,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
|
|||||||
@@ -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(
|
|
||||||
<LocalDeviceVerificationStateContext.Provider value={localDeviceVerified}>
|
|
||||||
<DecryptionFailureBody mxEvent={event} />
|
|
||||||
</LocalDeviceVerificationStateContext.Provider>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-45
@@ -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`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_DecryptionFailureBody mx_EventTile_content"
|
|
||||||
>
|
|
||||||
The sender has blocked you from receiving this message because your device is unverified
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`DecryptionFailureBody Should display "Unable to decrypt message" 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_DecryptionFailureBody mx_EventTile_content"
|
|
||||||
>
|
|
||||||
Unable to decrypt message
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`DecryptionFailureBody should handle messages from users who change identities after verification 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_DecryptionFailureBody mx_EventTile_content mx_DecryptionFailureSenderTrustRequirement"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<svg
|
|
||||||
class="mx_Icon mx_Icon_16"
|
|
||||||
fill="currentColor"
|
|
||||||
height="1em"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="1em"
|
|
||||||
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>
|
|
||||||
`;
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user