Move E2ePadlock to shared components (#33538)
* Move E2ePadlock to shared components * Add storybook images * Move translation to shared components * Fix E2ePadlock story accessibility * Update E2eMessageSharedIcon test for padlock role
This commit is contained in:
@@ -51,6 +51,8 @@ import { CircleIcon, CheckCircleIcon, ThreadsIcon } from "@vector-im/compound-de
|
||||
import {
|
||||
useCreateAutoDisposedViewModel,
|
||||
ActionBarView,
|
||||
E2ePadlock,
|
||||
E2ePadlockIcon,
|
||||
MessageTimestampView,
|
||||
PinnedMessageBadge,
|
||||
ReactionsRowButtonView,
|
||||
@@ -100,7 +102,6 @@ import { Icon as LateIcon } from "../../../../res/img/sensor.svg";
|
||||
import PinningUtils from "../../../utils/PinningUtils";
|
||||
import { EventPreview } from "./EventPreview";
|
||||
import { E2eMessageSharedIcon } from "./EventTile/E2eMessageSharedIcon.tsx";
|
||||
import { E2ePadlock, E2ePadlockIcon } from "./EventTile/E2ePadlock.tsx";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { CardContext } from "../right_panel/context";
|
||||
import { EventTileViewModel } from "../../../viewmodels/room/timeline/event-tile/EventTileViewModel";
|
||||
@@ -849,10 +850,28 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
}
|
||||
|
||||
if (this.state.shieldColour === EventShieldColour.GREY) {
|
||||
return <E2ePadlock icon={E2ePadlockIcon.Normal} title={shieldReasonMessage} />;
|
||||
return (
|
||||
<E2ePadlock
|
||||
className={
|
||||
// Timeline PCSS uses this app class as a layout hook for positioning and layout variants.
|
||||
"mx_EventTile_e2eIcon"
|
||||
}
|
||||
icon={E2ePadlockIcon.Normal}
|
||||
title={shieldReasonMessage}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// red, by elimination
|
||||
return <E2ePadlock icon={E2ePadlockIcon.Warning} title={shieldReasonMessage} />;
|
||||
return (
|
||||
<E2ePadlock
|
||||
className={
|
||||
// Timeline PCSS uses this app class as a layout hook for positioning and layout variants.
|
||||
"mx_EventTile_e2eIcon"
|
||||
}
|
||||
icon={E2ePadlockIcon.Warning}
|
||||
title={shieldReasonMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1607,11 +1626,29 @@ const SafeEventTile = (props: EventTileProps): JSX.Element => {
|
||||
export default SafeEventTile;
|
||||
|
||||
function E2ePadlockUnencrypted(): JSX.Element {
|
||||
return <E2ePadlock title={_t("common|unencrypted")} icon={E2ePadlockIcon.Warning} />;
|
||||
return (
|
||||
<E2ePadlock
|
||||
className={
|
||||
// Timeline PCSS uses this app class as a layout hook for positioning and layout variants.
|
||||
"mx_EventTile_e2eIcon"
|
||||
}
|
||||
title={_t("common|unencrypted")}
|
||||
icon={E2ePadlockIcon.Warning}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function E2ePadlockDecryptionFailure(): JSX.Element {
|
||||
return <E2ePadlock title={_t("timeline|undecryptable_tooltip")} icon={E2ePadlockIcon.DecryptionFailure} />;
|
||||
return (
|
||||
<E2ePadlock
|
||||
className={
|
||||
// Timeline PCSS uses this app class as a layout hook for positioning and layout variants.
|
||||
"mx_EventTile_e2eIcon"
|
||||
}
|
||||
title={_t("timeline|undecryptable_tooltip")}
|
||||
icon={E2ePadlockIcon.DecryptionFailure}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ISentReceiptProps {
|
||||
|
||||
@@ -7,10 +7,10 @@ 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 { E2ePadlock, E2ePadlockIcon } from "@element-hq/web-shared-components";
|
||||
|
||||
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 {
|
||||
@@ -41,5 +41,14 @@ export function E2eMessageSharedIcon(props: E2eMessageSharedIconParams): JSX.Ele
|
||||
userId: keyForwardingUserId,
|
||||
});
|
||||
|
||||
return <E2ePadlock icon={E2ePadlockIcon.Normal} title={tooltip} />;
|
||||
return (
|
||||
<E2ePadlock
|
||||
className={
|
||||
// Timeline PCSS uses this app class as a layout hook for positioning and layout variants.
|
||||
"mx_EventTile_e2eIcon"
|
||||
}
|
||||
icon={E2ePadlockIcon.Normal}
|
||||
title={tooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -3361,7 +3361,6 @@
|
||||
"download_action_downloading": "Downloading",
|
||||
"download_failed": "Download failed",
|
||||
"download_failed_description": "An error occurred while downloading this file",
|
||||
"e2e_state": "State of the end-to-end encryption",
|
||||
"edits": {
|
||||
"tooltip_label": "Edited at %(date)s. Click to view edits.",
|
||||
"tooltip_sub": "Click to view edits",
|
||||
|
||||
+13
-9
@@ -5,7 +5,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 { render } from "jest-matrix-react";
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { type RoomMember, type RoomState } from "matrix-js-sdk/src/matrix";
|
||||
@@ -14,7 +14,7 @@ import { E2eMessageSharedIcon } from "../../../../../../src/components/views/roo
|
||||
import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../../test-utils";
|
||||
|
||||
describe("E2eMessageSharedIcon", () => {
|
||||
it("renders correctly for a known user", () => {
|
||||
it("renders correctly for a known user", async () => {
|
||||
const mockClient = createTestClient();
|
||||
const mockMember = { rawDisplayName: "Bob" } as RoomMember;
|
||||
const mockState = {
|
||||
@@ -34,22 +34,26 @@ describe("E2eMessageSharedIcon", () => {
|
||||
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.",
|
||||
await waitFor(() =>
|
||||
expect(result.container.firstChild).toHaveAccessibleName(
|
||||
"Bob (@bob:example.com) shared this message since you were not in the room when it was sent.",
|
||||
),
|
||||
);
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly for an unknown user", () => {
|
||||
it("renders correctly for an unknown user", async () => {
|
||||
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.",
|
||||
await waitFor(() =>
|
||||
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.",
|
||||
),
|
||||
);
|
||||
expect(result.container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
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();
|
||||
});
|
||||
});
|
||||
+4
-2
@@ -5,8 +5,9 @@ exports[`E2eMessageSharedIcon renders correctly for a known user 1`] = `
|
||||
<div
|
||||
aria-label="State of the end-to-end encryption"
|
||||
aria-labelledby="_r_0_"
|
||||
class="mx_EventTile_e2eIcon"
|
||||
class="_e2ePadlock_ohpuo_8 mx_EventTile_e2eIcon"
|
||||
data-testid="e2e-padlock"
|
||||
role="img"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
@@ -35,8 +36,9 @@ exports[`E2eMessageSharedIcon renders correctly for an unknown user 1`] = `
|
||||
<div
|
||||
aria-label="State of the end-to-end encryption"
|
||||
aria-labelledby="_r_6_"
|
||||
class="mx_EventTile_e2eIcon"
|
||||
class="_e2ePadlock_ohpuo_8 mx_EventTile_e2eIcon"
|
||||
data-testid="e2e-padlock"
|
||||
role="img"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
Reference in New Issue
Block a user