Refactor MKeyVerificationRequest to shared view MVVM (#33461)

* Refactor key verification request to shared view

* Fix prettier

* add tests to pass coverage
This commit is contained in:
Zack
2026-05-11 11:36:07 +00:00
committed by GitHub
parent 7af8beea40
commit c62acc6634
16 changed files with 699 additions and 206 deletions
+1
View File
@@ -40,6 +40,7 @@ export * from "./room/timeline/event-tile/actions/ActionBarView";
export * from "./room/timeline/event-tile/EventTileView/DisambiguatedProfile";
export * from "./room/timeline/event-tile/EventTileView/EncryptionEventView";
export * from "./room/timeline/event-tile/EventTileView/EventTileBubble";
export * from "./room/timeline/event-tile/EventTileView/MKeyVerificationRequestView";
export * from "./room/timeline/event-tile/EventTileView/PinnedMessageBadge";
export * from "./room/timeline/event-tile/EventTileView/TextualEventView";
export * from "./room/timeline/event-tile/body/AudioPlayerView";
@@ -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.
*/
.content {
svg {
color: var(--cpd-color-text-primary);
}
}
@@ -0,0 +1,60 @@
/*
* 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 { MKeyVerificationRequestView, type MKeyVerificationRequestViewSnapshot } from "./MKeyVerificationRequestView";
type MKeyVerificationRequestViewProps = MKeyVerificationRequestViewSnapshot & {
className?: string;
};
const MKeyVerificationRequestViewWrapperImpl = ({
className,
...snapshot
}: MKeyVerificationRequestViewProps): JSX.Element => {
const vm = useMockedViewModel(snapshot, {});
return <MKeyVerificationRequestView vm={vm} className={className} />;
};
const MKeyVerificationRequestViewWrapper = withViewDocs(
MKeyVerificationRequestViewWrapperImpl,
MKeyVerificationRequestView,
);
const meta = {
title: "Timeline/Timeline Event/MKeyVerificationRequestView",
component: MKeyVerificationRequestViewWrapper,
tags: ["autodocs"],
args: {
title: "Alice wants to verify",
subtitle: "Alice (@alice:example.org)",
className: "",
},
} satisfies Meta<typeof MKeyVerificationRequestViewWrapper>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Received: Story = {};
export const SentByMe: Story = {
args: {
title: "You sent a verification request",
subtitle: "Bob (@bob:example.org)",
},
};
export const WithTimestamp: Story = {
args: {
timestamp: <span>14:56</span>,
},
};
@@ -0,0 +1,65 @@
/*
* 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 { composeStories } from "@storybook/react-vite";
import { render, screen } from "@test-utils";
import React from "react";
import { describe, expect, it } from "vitest";
import { MockViewModel } from "../../../../../core/viewmodel";
import { MKeyVerificationRequestView } from "./MKeyVerificationRequestView";
import * as stories from "./MKeyVerificationRequestView.stories";
const { Received, SentByMe, WithTimestamp } = composeStories(stories);
describe("MKeyVerificationRequestView", () => {
it("renders the Received story", () => {
const { container } = render(<Received />);
expect(container).toMatchSnapshot();
expect(screen.getByText("Alice wants to verify")).toBeInTheDocument();
expect(screen.getByText("Alice (@alice:example.org)")).toBeInTheDocument();
});
it("renders the SentByMe story", () => {
const { container } = render(<SentByMe />);
expect(container).toMatchSnapshot();
expect(screen.getByText("You sent a verification request")).toBeInTheDocument();
expect(screen.getByText("Bob (@bob:example.org)")).toBeInTheDocument();
});
it("renders a timestamp", () => {
const { container } = render(<WithTimestamp />);
expect(container).toMatchSnapshot();
expect(screen.getByText("14:56")).toBeInTheDocument();
});
it("applies a custom className to the root element", () => {
const vm = new MockViewModel({
title: "Alice wants to verify",
subtitle: "Alice (@alice:example.org)",
});
const { container } = render(<MKeyVerificationRequestView vm={vm} className="custom-verification" />);
expect(container.firstChild).toHaveClass("custom-verification");
});
it("forwards the provided ref to the root element", () => {
const ref = React.createRef<HTMLDivElement>() as React.RefObject<HTMLDivElement>;
const vm = new MockViewModel({
title: "Alice wants to verify",
subtitle: "Alice (@alice:example.org)",
});
render(<MKeyVerificationRequestView vm={vm} ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLDivElement);
expect(ref.current).toHaveTextContent("Alice wants to verify");
});
});
@@ -0,0 +1,69 @@
/*
* 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 classNames from "classnames";
import React, { type JSX } from "react";
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
import { EventTileBubble } from "../EventTileBubble";
import styles from "./MKeyVerificationRequestView.module.css";
export interface MKeyVerificationRequestViewSnapshot {
/**
* Main title text for the verification request.
*/
title: string;
/**
* Label for the other user involved in the request.
*/
subtitle: string;
/**
* Optional timestamp element rendered in the EventTileBubble footer slot.
*/
timestamp?: JSX.Element;
}
export type MKeyVerificationRequestViewModel = ViewModel<MKeyVerificationRequestViewSnapshot>;
export interface MKeyVerificationRequestViewProps {
/**
* ViewModel providing the current verification request snapshot.
*/
vm: MKeyVerificationRequestViewModel;
/**
* Optional CSS classes passed through to EventTileBubble.
*/
className?: string;
/**
* Optional Ref forwarded to the root DOM element.
*/
ref?: React.RefObject<HTMLDivElement>;
}
/**
* Renders a timeline bubble describing a key verification request message.
*/
export function MKeyVerificationRequestView({
vm,
className,
ref,
}: Readonly<MKeyVerificationRequestViewProps>): JSX.Element {
const { title, subtitle, timestamp } = useViewModel(vm);
return (
<EventTileBubble
icon={<LockSolidIcon />}
className={classNames(styles.content, className)}
title={title}
subtitle={subtitle}
ref={ref}
>
{timestamp}
</EventTileBubble>
);
}
@@ -0,0 +1,94 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`MKeyVerificationRequestView > renders a timestamp 1`] = `
<div>
<div
class="EventTileBubble-module_container MKeyVerificationRequestView-module_content"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
<div
class="EventTileBubble-module_title"
>
Alice wants to verify
</div>
<div
class="EventTileBubble-module_subtitle"
>
Alice (@alice:example.org)
</div>
<span>
14:56
</span>
</div>
</div>
`;
exports[`MKeyVerificationRequestView > renders the Received story 1`] = `
<div>
<div
class="EventTileBubble-module_container MKeyVerificationRequestView-module_content"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
<div
class="EventTileBubble-module_title"
>
Alice wants to verify
</div>
<div
class="EventTileBubble-module_subtitle"
>
Alice (@alice:example.org)
</div>
</div>
</div>
`;
exports[`MKeyVerificationRequestView > renders the SentByMe story 1`] = `
<div>
<div
class="EventTileBubble-module_container MKeyVerificationRequestView-module_content"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
<div
class="EventTileBubble-module_title"
>
You sent a verification request
</div>
<div
class="EventTileBubble-module_subtitle"
>
Bob (@bob:example.org)
</div>
</div>
</div>
`;
@@ -0,0 +1,13 @@
/*
* 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 {
MKeyVerificationRequestView,
type MKeyVerificationRequestViewProps,
type MKeyVerificationRequestViewSnapshot,
type MKeyVerificationRequestViewModel,
} from "./MKeyVerificationRequestView";