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

@@ -28,6 +28,7 @@ import {
EventShieldReason,
} from "matrix-js-sdk/src/crypto-api";
import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing";
import { getByTestId } from "@testing-library/dom";
import EventTile, { type EventTileProps } from "../../../../../src/components/views/rooms/EventTile";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
@@ -333,6 +334,28 @@ describe("EventTile", () => {
expect(e2eIcons[0]).toHaveAccessibleName(expectedText);
});
it("shows the correct reason code for a forwarded message", async () => {
mxEvent = await mkEncryptedMatrixEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
plainType: "m.room.message",
sender: "@alice:example.org",
roomId: room.roomId,
});
// @ts-ignore assignment to private member
mxEvent.keyForwardedBy = "@bob:example.org";
eventToEncryptionInfoMap.set(mxEvent.getId()!, {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
} as EventEncryptionInfo);
const { container } = getComponent();
const e2eIcon = await waitFor(() => getByTestId(container, "e2e-padlock"));
expect(e2eIcon).toHaveAccessibleName(
"@bob:example.org (@bob:example.org) shared this message since you were not in the room when it was sent.",
);
});
describe("undecryptable event", () => {
filterConsole("Error decrypting event");

View File

@@ -0,0 +1,55 @@
/*
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 { render } from "jest-matrix-react";
import React from "react";
import { mocked } from "jest-mock";
import { type RoomMember, type RoomState } from "matrix-js-sdk/src/matrix";
import { E2eMessageSharedIcon } from "../../../../../../src/components/views/rooms/EventTile/E2eMessageSharedIcon.tsx";
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../../test-utils";
describe("E2eMessageSharedIcon", () => {
it("renders correctly for a known user", () => {
const mockClient = createTestClient();
const mockMember = { rawDisplayName: "Bob" } as RoomMember;
const mockState = {
getMember: (userId) => {
expect(userId).toEqual("@bob:example.com");
return mockMember;
},
} as RoomState;
const mockRoom = mkStubRoom("!roomId", undefined, mockClient, mockState);
mocked(mockClient.getRoom).mockImplementation((roomId) => {
expect(roomId).toEqual("!roomId");
return mockRoom;
});
const result = render(
<E2eMessageSharedIcon keyForwardingUserId="@bob:example.com" roomId="!roomId" />,
withClientContextRenderOptions(mockClient),
);
expect(result.container).toMatchSnapshot();
expect(result.container.firstChild).toHaveAccessibleName(
"Bob (@bob:example.com) shared this message since you were not in the room when it was sent.",
);
});
it("renders correctly for an unknown user", () => {
const mockClient = createTestClient();
const result = render(
<E2eMessageSharedIcon keyForwardingUserId="@bob:example.com" roomId="!roomId" />,
withClientContextRenderOptions(mockClient),
);
expect(result.container).toMatchSnapshot();
expect(result.container.firstChild).toHaveAccessibleName(
"@bob:example.com (@bob:example.com) shared this message since you were not in the room when it was sent.",
);
});
});

View File

@@ -0,0 +1,28 @@
/*
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 { render } from "jest-matrix-react";
import React from "react";
import { E2ePadlock, E2ePadlockIcon } from "../../../../../../src/components/views/rooms/EventTile/E2ePadlock.tsx";
describe("E2ePadlock", () => {
it("renders a 'Normal' icon", () => {
const result = render(<E2ePadlock icon={E2ePadlockIcon.Normal} title="Test tooltip" />);
expect(result.asFragment()).toMatchSnapshot();
});
it("renders a 'Warning' icon", () => {
const result = render(<E2ePadlock icon={E2ePadlockIcon.Warning} title="Bad" />);
expect(result.asFragment()).toMatchSnapshot();
});
it("renders a 'DecryptionFailure' icon", () => {
const result = render(<E2ePadlock icon={E2ePadlockIcon.DecryptionFailure} title="UTD" />);
expect(result.asFragment()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,61 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`E2eMessageSharedIcon renders correctly for a known user 1`] = `
<div>
<div
aria-label="State of the end-to-end encryption"
aria-labelledby="_r_0_"
class="mx_EventTile_e2eIcon"
data-testid="e2e-padlock"
tabindex="0"
>
<svg
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
`;
exports[`E2eMessageSharedIcon renders correctly for an unknown user 1`] = `
<div>
<div
aria-label="State of the end-to-end encryption"
aria-labelledby="_r_6_"
class="mx_EventTile_e2eIcon"
data-testid="e2e-padlock"
tabindex="0"
>
<svg
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
`;

View File

@@ -0,0 +1,81 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`E2ePadlock renders a 'DecryptionFailure' icon 1`] = `
<DocumentFragment>
<div
aria-label="State of the end-to-end encryption"
aria-labelledby="_r_c_"
class="mx_EventTile_e2eIcon"
data-testid="e2e-padlock"
tabindex="0"
>
<svg
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.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 22"
/>
</svg>
</div>
</DocumentFragment>
`;
exports[`E2ePadlock renders a 'Normal' icon 1`] = `
<DocumentFragment>
<div
aria-label="State of the end-to-end encryption"
aria-labelledby="_r_0_"
class="mx_EventTile_e2eIcon"
data-testid="e2e-padlock"
tabindex="0"
>
<svg
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</div>
</DocumentFragment>
`;
exports[`E2ePadlock renders a 'Warning' icon 1`] = `
<DocumentFragment>
<div
aria-label="State of the end-to-end encryption"
aria-labelledby="_r_6_"
class="mx_EventTile_e2eIcon"
data-testid="e2e-padlock"
tabindex="0"
>
<svg
color="var(--cpd-color-icon-critical-primary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.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 22"
/>
</svg>
</div>
</DocumentFragment>
`;