Refactor E2eMessageSharedIcon to MVVM (#33544)
* Refactor E2eMessageSharedIcon to MVVM * FIx Prettier and i18n * Fix prettier * Remove legacy E2eMessageSharedIcon component * Cover E2eMessageSharedIcon MVVM updates * Fix prettier * Update apps/web/src/components/views/rooms/EventTile.tsx Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * Skip unknown E2E shared icon visual * Track E2E shared icon listener disposal * Remove E2E shared icon room state guard * Remove E2E shared icon client resync * Use built-in disposables for shared icon VM * Update shared icon VM without React key --------- Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -251,6 +251,7 @@
|
||||
"label": "Message Actions",
|
||||
"view_in_room": "View in room"
|
||||
},
|
||||
"message_shared_by": "%(displayName)s (%(userId)s) shared this message since you were not in the room when it was sent.",
|
||||
"message_timestamp_received_at": "Received at: %(dateTime)s",
|
||||
"message_timestamp_sent_at": "Sent at: %(dateTime)s",
|
||||
"mjolnir": {
|
||||
|
||||
@@ -40,6 +40,7 @@ export * from "./room/timeline/DateSeparatorView";
|
||||
export * from "./room/timeline/TimelineSeparator";
|
||||
export * from "./room/timeline/event-tile/actions/ActionBarView";
|
||||
export * from "./room/timeline/event-tile/EventTileView/DisambiguatedProfile";
|
||||
export * from "./room/timeline/event-tile/EventTileView/E2eMessageSharedIcon";
|
||||
export * from "./room/timeline/event-tile/EventTileView/E2ePadlock";
|
||||
export * from "./room/timeline/event-tile/EventTileView/EncryptionEventView";
|
||||
export * from "./room/timeline/event-tile/call";
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
||||
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
||||
import { E2eMessageSharedIconView, type E2eMessageSharedIconViewSnapshot } from "./E2eMessageSharedIconView";
|
||||
|
||||
type E2eMessageSharedIconProps = E2eMessageSharedIconViewSnapshot & { className?: string };
|
||||
|
||||
const E2eMessageSharedIconViewWrapperImpl = ({ className, ...rest }: E2eMessageSharedIconProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(rest, {});
|
||||
|
||||
return <E2eMessageSharedIconView vm={vm} className={className} />;
|
||||
};
|
||||
const E2eMessageSharedIconViewWrapper = withViewDocs(E2eMessageSharedIconViewWrapperImpl, E2eMessageSharedIconView);
|
||||
|
||||
const meta = {
|
||||
title: "Timeline/Timeline Event/E2eMessageSharedIconView",
|
||||
component: E2eMessageSharedIconViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
displayName: "Bob",
|
||||
userId: "@bob:example.com",
|
||||
className: "",
|
||||
},
|
||||
} satisfies Meta<typeof E2eMessageSharedIconViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const UnknownUser: Story = {
|
||||
tags: ["skip-test"],
|
||||
args: {
|
||||
displayName: "@bob:example.com",
|
||||
userId: "@bob:example.com",
|
||||
},
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 from "react";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen } from "@test-utils";
|
||||
|
||||
import { MockViewModel } from "../../../../../core/viewmodel";
|
||||
import {
|
||||
E2eMessageSharedIconView,
|
||||
type E2eMessageSharedIconViewModel,
|
||||
type E2eMessageSharedIconViewSnapshot,
|
||||
} from "./E2eMessageSharedIconView";
|
||||
import * as stories from "./E2eMessageSharedIcon.stories";
|
||||
|
||||
const { Default, UnknownUser } = composeStories(stories);
|
||||
|
||||
describe("E2eMessageSharedIconView", () => {
|
||||
it("renders the default shared-message icon", () => {
|
||||
const { container } = render(<Default />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders an unknown user tooltip state", () => {
|
||||
const { container } = render(<UnknownUser />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("applies a custom className to the icon container", () => {
|
||||
const vm = new MockViewModel<E2eMessageSharedIconViewSnapshot>({
|
||||
displayName: "Bob",
|
||||
userId: "@bob:example.com",
|
||||
}) as E2eMessageSharedIconViewModel;
|
||||
|
||||
render(<E2eMessageSharedIconView vm={vm} className="custom-shared-icon" />);
|
||||
|
||||
expect(screen.getByTestId("e2e-padlock")).toHaveClass("custom-shared-icon");
|
||||
});
|
||||
|
||||
it("uses the shared translation for the icon accessible name", () => {
|
||||
render(<Default />);
|
||||
|
||||
expect(screen.getByTestId("e2e-padlock")).toHaveAccessibleName(
|
||||
"Bob (@bob:example.com) shared this message since you were not in the room when it was sent.",
|
||||
);
|
||||
});
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { _t } from "../../../../../core/i18n/i18n";
|
||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||
import { E2ePadlock, E2ePadlockIcon } from "../E2ePadlock";
|
||||
|
||||
export interface E2eMessageSharedIconViewSnapshot {
|
||||
/**
|
||||
* Display name for the user who shared keys for the message.
|
||||
*/
|
||||
displayName: string;
|
||||
/**
|
||||
* User ID for the user who shared keys for the message.
|
||||
*/
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export type E2eMessageSharedIconViewModel = ViewModel<E2eMessageSharedIconViewSnapshot>;
|
||||
|
||||
interface E2eMessageSharedIconViewProps {
|
||||
/**
|
||||
* ViewModel providing the localized tooltip.
|
||||
*/
|
||||
vm: E2eMessageSharedIconViewModel;
|
||||
/**
|
||||
* Optional CSS class name applied to the icon container.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the end-to-end encryption icon used for messages whose keys were
|
||||
* shared by another room member.
|
||||
*/
|
||||
export function E2eMessageSharedIconView({ vm, className }: Readonly<E2eMessageSharedIconViewProps>): JSX.Element {
|
||||
const { displayName, userId } = useViewModel(vm);
|
||||
// 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("timeline|message_shared_by", { displayName, userId });
|
||||
|
||||
return <E2ePadlock className={className} icon={E2ePadlockIcon.Normal} title={tooltip} />;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`E2eMessageSharedIconView > renders an unknown user tooltip state 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="State of the end-to-end encryption"
|
||||
aria-labelledby="react-use-id-1"
|
||||
class="E2ePadlock-module_e2ePadlock E2ePadlock-module_normal"
|
||||
data-testid="e2e-padlock"
|
||||
role="img"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
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[`E2eMessageSharedIconView > renders the default shared-message icon 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="State of the end-to-end encryption"
|
||||
aria-labelledby="react-use-id-1"
|
||||
class="E2ePadlock-module_e2ePadlock E2ePadlock-module_normal"
|
||||
data-testid="e2e-padlock"
|
||||
role="img"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
`;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
E2eMessageSharedIconView,
|
||||
type E2eMessageSharedIconViewModel,
|
||||
type E2eMessageSharedIconViewSnapshot,
|
||||
} from "./E2eMessageSharedIconView";
|
||||
Reference in New Issue
Block a user