* Add Actions to ViewModel utility types and specify `this: void` signature Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add https://typescript-eslint.io/rules/unbound-method/ linter to shared-components also fix stray lint config which doesn't apply to SC Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add https://typescript-eslint.io/rules/unbound-method/ linter to element-web Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix genuine issues identified by the linter Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Specify this:void on i18napi Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update Module API Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add comment for MapToVoidThis Added utility type to map VM actions to unbound functions. --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
177 lines
6.5 KiB
TypeScript
177 lines
6.5 KiB
TypeScript
/*
|
|
* 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<HTMLDivElement>;
|
|
}
|
|
|
|
/**
|
|
* 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>
|
|
);
|
|
}
|