Show "Bob shared this message" on messages shared via MSC4268 (#31684)

* Factor out E2ePadlock to its own file

* Show "Bob shared this message" on messages shared via MSC4268

If we received the keys for a given message from another user, indicate that in
the timeline, rather than just saying "authenticity not guaranteed"

* Apply suggestions from code review

Co-authored-by: Florian Duros <florianduros@element.io>

* Address review comments

* Move E2ePadlock to shared-components

* update snapshots

* Revert "update snapshots"

This reverts commit 751e31f9db901fda085143c98e5dffa3d2b4c099.

* Revert "Move E2ePadlock to shared-components"

This reverts commit 172ef9f70ab26fd36b0ac854379cfd3371d22c18.

---------

Co-authored-by: Florian Duros <florianduros@element.io>
This commit is contained in:
Richard van der Hoff
2026-01-16 17:22:42 +00:00
committed by GitHub
parent d9a4858b1d
commit 92a6db5912
9 changed files with 375 additions and 56 deletions

View File

@@ -35,12 +35,7 @@ import {
} from "matrix-js-sdk/src/crypto-api";
import { Tooltip } from "@vector-im/compound-web";
import { uniqueId } from "lodash";
import {
CircleIcon,
ErrorSolidIcon,
InfoIcon,
CheckCircleIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { CircleIcon, CheckCircleIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import ReplyChain from "../elements/ReplyChain";
import { _t } from "../../../languageHandler";
@@ -89,6 +84,8 @@ import PinningUtils from "../../../utils/PinningUtils";
import { PinnedMessageBadge } from "../messages/PinnedMessageBadge";
import { EventPreview } from "./EventPreview";
import { ElementCallEventType } from "../../../call-types";
import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx";
import { E2ePadlock, E2ePadlockIcon } from "./EventTile/E2ePadlock.tsx";
export type GetRelationsForEvent = (
eventId: string,
@@ -740,6 +737,14 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
}
}
if (this.state.shieldReason === EventShieldReason.AUTHENTICITY_NOT_GUARANTEED) {
// This may happen if the message was forwarded to us by another user, in which case we can show a better message
const forwarder = this.props.mxEvent.getKeyForwardingUser();
if (forwarder) {
return <E2eMessageSharedIcon keyForwardingUserId={forwarder} roomId={ev.getRoomId()!} />;
}
}
if (this.state.shieldColour !== EventShieldColour.NONE) {
let shieldReasonMessage: string;
switch (this.state.shieldReason) {
@@ -1513,58 +1518,12 @@ const SafeEventTile = (props: EventTileProps): JSX.Element => {
};
export default SafeEventTile;
function E2ePadlockUnencrypted(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
return <E2ePadlock title={_t("common|unencrypted")} icon={E2ePadlockIcon.Warning} {...props} />;
function E2ePadlockUnencrypted(): JSX.Element {
return <E2ePadlock title={_t("common|unencrypted")} icon={E2ePadlockIcon.Warning} />;
}
function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "icon">): JSX.Element {
return (
<E2ePadlock title={_t("timeline|undecryptable_tooltip")} icon={E2ePadlockIcon.DecryptionFailure} {...props} />
);
}
enum E2ePadlockIcon {
/** Compound Info icon in grey */
Normal = "normal",
/** Compound ErrorSolid icon in red */
Warning = "warning",
/** Compound ErrorSolid icon in grey */
DecryptionFailure = "decryption_failure",
}
interface IE2ePadlockProps {
icon: E2ePadlockIcon;
title: string;
}
class E2ePadlock extends React.Component<IE2ePadlockProps> {
private static icons: Record<E2ePadlockIcon, JSX.Element> = {
[E2ePadlockIcon.Normal]: <InfoIcon color="var(--cpd-color-icon-tertiary)" />,
[E2ePadlockIcon.Warning]: <ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />,
[E2ePadlockIcon.DecryptionFailure]: <ErrorSolidIcon color="var(--cpd-color-icon-tertiary)" />,
};
public constructor(props: IE2ePadlockProps) {
super(props);
this.state = {
hover: false,
};
}
public render(): ReactNode {
// We specify isTriggerInteractive=true and make the div interactive manually as a workaround for
// https://github.com/element-hq/compound/issues/294
return (
<Tooltip label={this.props.title} isTriggerInteractive={true}>
<div className="mx_EventTile_e2eIcon" tabIndex={0} aria-label={_t("timeline|e2e_state")}>
{E2ePadlock.icons[this.props.icon]}
</div>
</Tooltip>
);
}
function E2ePadlockDecryptionFailure(): JSX.Element {
return <E2ePadlock title={_t("timeline|undecryptable_tooltip")} icon={E2ePadlockIcon.DecryptionFailure} />;
}
interface ISentReceiptProps {

View File

@@ -0,0 +1,45 @@
/*
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 { EventTimeline } from "matrix-js-sdk/src/matrix";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx";
import { _t } from "../../../../languageHandler.tsx";
import { E2ePadlock, E2ePadlockIcon } from "./E2ePadlock.tsx";
/** The React properties of an {@link E2eMessageSharedIcon}. */
interface E2eMessageSharedIconParams {
/** The ID of the user who shared the keys. */
keyForwardingUserId: string;
/** The ID of the room that contains the event whose keys were shared. Used to find the displayname of the user who shared the keys. */
roomId: string;
}
/**
* A small icon with tooltip, used as part of an {@link EventTile}, which indicates that the key to this event
* was shared with us by another user.
*
* An alternative to the {@link E2ePadlock} component, which is used for UTD events and other error cases.
*/
export function E2eMessageSharedIcon(props: E2eMessageSharedIconParams): JSX.Element {
const { roomId, keyForwardingUserId } = props;
const client = useMatrixClientContext();
const roomState = client.getRoom(roomId)?.getLiveTimeline()?.getState(EventTimeline.FORWARDS);
const forwardingMember = roomState?.getMember(keyForwardingUserId);
// We always disambiguate the user, since we need to prevent users from forging a disambiguation, and
// the ToolTip component doesn't support putting styling inside a label.
const tooltip = _t("encryption|message_shared_by", {
displayName: forwardingMember?.rawDisplayName ?? keyForwardingUserId,
userId: keyForwardingUserId,
});
return <E2ePadlock icon={E2ePadlockIcon.Normal} title={tooltip} />;
}

View File

@@ -0,0 +1,66 @@
/*
Copyright 2025 Vector Creations Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2015-2023 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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 ReactNode } from "react";
import { ErrorSolidIcon, InfoIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Tooltip } from "@vector-im/compound-web";
import { _t } from "../../../../languageHandler.tsx";
/**
* The icon to display in an {@link E2ePadlock}.
*/
export enum E2ePadlockIcon {
/** Compound Info icon in grey */
Normal = "normal",
/** Compound ErrorSolid icon in red */
Warning = "warning",
/** Compound ErrorSolid icon in grey */
DecryptionFailure = "decryption_failure",
}
interface IE2ePadlockProps {
/** The icon to display. */
icon: E2ePadlockIcon;
/** The tooltip for the icon, displayed on hover. */
title: string;
}
const icons = {
[E2ePadlockIcon.Normal]: <InfoIcon color="var(--cpd-color-icon-tertiary)" />,
[E2ePadlockIcon.Warning]: <ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />,
[E2ePadlockIcon.DecryptionFailure]: <ErrorSolidIcon color="var(--cpd-color-icon-tertiary)" />,
};
/**
* A small icon with tooltip, used in the left margin of an {@link EventTile}, which indicates a problem
* with an encrypted event.
*
* The icon is rendered with `data-testid="e2e-padlock"`.
*/
export function E2ePadlock(props: IE2ePadlockProps): ReactNode {
// We specify isTriggerInteractive=true and make the div interactive manually as a workaround for
// https://github.com/element-hq/compound/issues/294
return (
<Tooltip label={props.title} isTriggerInteractive={true}>
<div
data-testid="e2e-padlock"
className="mx_EventTile_e2eIcon"
tabIndex={0}
aria-label={_t("timeline|e2e_state")}
>
{icons[props.icon]}
</div>
</Tooltip>
);
}

View File

@@ -978,6 +978,7 @@
"import_invalid_passphrase": "Authentication check failed: incorrect password?",
"key_storage_out_of_sync": "Your key storage is out of sync.",
"key_storage_out_of_sync_description": "Confirm your recovery key to maintain access to your key storage and message history.",
"message_shared_by": "%(displayName)s (%(userId)s) shared this message since you were not in the room when it was sent.",
"messages_not_secure": {
"cause_1": "Your homeserver",
"cause_2": "The homeserver the user you're verifying is connected to",