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:
@@ -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".
|
||||
|
||||
@@ -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 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<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
||||
@@ -248,7 +250,7 @@ export default class MessageEvent extends React.Component<IProps> 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<IBodyProps & { WrappedBodyType: React
|
||||
<TextualBody {...{ ...props, ref: undefined }} />
|
||||
</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.
|
||||
*/
|
||||
|
||||
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<EventTileProps, IState>
|
||||
{this.props.mxEvent.isRedacted() ? (
|
||||
<RedactedBody mxEvent={this.props.mxEvent} />
|
||||
) : this.props.mxEvent.isDecryptionFailure() ? (
|
||||
<DecryptionFailureBody mxEvent={this.props.mxEvent} />
|
||||
<DecryptionFailureBodyWrapper mxEvent={this.props.mxEvent} />
|
||||
) : (
|
||||
<EventPreview mxEvent={this.props.mxEvent} />
|
||||
)}
|
||||
@@ -1569,3 +1571,23 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
|
||||
</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 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);
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user