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:
Zack
2026-05-28 10:01:39 +02:00
committed by GitHub
parent 013413c27a
commit 34c388f760
14 changed files with 516 additions and 121 deletions
@@ -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": {
+1
View File
@@ -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";
@@ -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",
},
};
@@ -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.",
);
});
});
@@ -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} />;
}
@@ -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>
`;
@@ -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";