mirror of
https://github.com/element-hq/element-web.git
synced 2026-01-16 23:01:06 +00:00
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"
This commit is contained in:
parent
424213e6ef
commit
bf1c8d0d45
6 changed files with 193 additions and 0 deletions
|
|
@ -83,6 +83,7 @@ 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 = (
|
||||
|
|
@ -735,6 +736,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) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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, useContext } from "react";
|
||||
import { EventTimeline } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext.tsx";
|
||||
import { _t } from "../../../../languageHandler.tsx";
|
||||
import { E2ePadlock, E2ePadlockIcon } from "./E2ePadlock.tsx";
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
/** 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;
|
||||
}): JSX.Element {
|
||||
const { roomId, keyForwardingUserId } = props;
|
||||
const client = useContext(MatrixClientContext);
|
||||
|
||||
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} />;
|
||||
}
|
||||
|
|
@ -980,6 +980,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",
|
||||
|
|
|
|||
|
|
@ -332,6 +332,30 @@ 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();
|
||||
await flushPromises();
|
||||
|
||||
const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon");
|
||||
expect(e2eIcons).toHaveLength(1);
|
||||
expect(e2eIcons[0]).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");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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 MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext.tsx";
|
||||
import { createTestClient, mkStubRoom } 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(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<E2eMessageSharedIcon keyForwardingUserId="@bob:example.com" roomId="!roomId" />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<E2eMessageSharedIcon keyForwardingUserId="@bob:example.com" roomId="!roomId" />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// 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"
|
||||
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"
|
||||
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>
|
||||
`;
|
||||
Loading…
Add table
Reference in a new issue