Refactor EventPreview to shared MVVM (#33646)

* Refactor EventPreview to shared MVVM

* Fix EventPreviewView export formatting

* Snapshots images for stories

* Deduplicate event preview formatting

* Handle event preview update failures

* Coalesce event preview updates

* Wait for event preview test expectations
This commit is contained in:
Zack
2026-06-02 10:36:28 +02:00
committed by GitHub
parent 1b52311f2e
commit 1c04814c2b
21 changed files with 765 additions and 271 deletions
+1
View File
@@ -43,6 +43,7 @@ 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/EventTileView/EventPreviewView";
export * from "./room/timeline/event-tile/call";
export * from "./room/timeline/event-tile/EventTileView/EventTileBubble";
export * from "./room/timeline/event-tile/EventTileView/MJitsiWidgetEventView";
@@ -0,0 +1,17 @@
/*
* 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.
*/
.eventPreview {
font: var(--cpd-font-body-sm-regular);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.eventPreview strong {
font: var(--cpd-font-body-sm-semibold);
}
@@ -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 React, { type JSX } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useMockedViewModel } from "../../../../../core/viewmodel";
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
import { EventPreviewView, type EventPreviewViewSnapshot } from "./EventPreviewView";
type WrapperProps = EventPreviewViewSnapshot & {
className?: string;
};
const EventPreviewViewWrapperImpl = ({ className, ...snapshotProps }: WrapperProps): JSX.Element => {
const vm = useMockedViewModel(snapshotProps, {});
return <EventPreviewView vm={vm} className={className} />;
};
const EventPreviewViewWrapper = withViewDocs(EventPreviewViewWrapperImpl, EventPreviewView);
const meta = {
title: "Timeline/EventTile/EventPreviewView",
component: EventPreviewViewWrapper,
tags: ["autodocs"],
args: {
isVisible: true,
previewContent: "A short text message preview",
previewTooltip: "A short text message preview",
},
} satisfies Meta<typeof EventPreviewViewWrapper>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const WithPrefix: Story = {
args: {
previewContent: (
<>
<strong>Image:</strong> city-map.png
</>
),
previewTooltip: undefined,
},
};
export const Hidden: Story = {
args: {
isVisible: false,
},
};
@@ -0,0 +1,50 @@
/*
* 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 { render, screen } from "@test-utils";
import { describe, expect, it } from "vitest";
import { MockViewModel } from "../../../../../core/viewmodel";
import { EventPreviewView, type EventPreviewViewModel, type EventPreviewViewSnapshot } from "./EventPreviewView";
import * as stories from "./EventPreviewView.stories";
const { Default, WithPrefix, Hidden } = composeStories(stories);
describe("EventPreviewView", () => {
it("renders the default event preview", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders a prefixed event preview", () => {
const { container } = render(<WithPrefix />);
expect(container).toMatchSnapshot();
});
it("does not render when hidden", () => {
render(<Hidden />);
expect(screen.queryByText("A short text message preview")).not.toBeInTheDocument();
});
it("applies custom span props without using native title tooltips", () => {
const vm = new MockViewModel<EventPreviewViewSnapshot>({
isVisible: true,
previewContent: "Preview text",
previewTooltip: "Preview text",
}) as EventPreviewViewModel;
render(<EventPreviewView vm={vm} className="custom-preview" data-testid="event-preview" />);
expect(screen.getByTestId("event-preview")).toHaveClass("mx_EventPreview", "custom-preview");
expect(screen.getByTestId("event-preview")).not.toHaveAttribute("title");
});
});
@@ -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 ComponentPropsWithoutRef, type JSX, type ReactNode } from "react";
import classNames from "classnames";
import { Tooltip } from "@vector-im/compound-web";
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
import styles from "./EventPreviewView.module.css";
export interface EventPreviewViewSnapshot {
/**
* Controls whether the preview should render.
*/
isVisible: boolean;
/**
* Rendered preview content.
*/
previewContent?: ReactNode;
/**
* Optional styled tooltip text for the preview content.
*/
previewTooltip?: string;
}
export type EventPreviewViewModel = ViewModel<EventPreviewViewSnapshot>;
type EventPreviewViewProps = Omit<ComponentPropsWithoutRef<"span">, "children" | "title"> & {
/**
* The view model for the event preview.
*/
vm: EventPreviewViewModel;
};
/**
* Renders a compact preview of an event.
*/
export function EventPreviewView({ vm, className, ...props }: Readonly<EventPreviewViewProps>): JSX.Element {
const { isVisible, previewContent, previewTooltip } = useViewModel(vm);
if (!isVisible || !previewContent) {
return <></>;
}
const preview = (
<span {...props} className={classNames("mx_EventPreview", styles.eventPreview, className)}>
{previewContent}
</span>
);
if (!previewTooltip) {
return preview;
}
return <Tooltip description={previewTooltip}>{preview}</Tooltip>;
}
@@ -0,0 +1,24 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`EventPreviewView > renders a prefixed event preview 1`] = `
<div>
<span
class="mx_EventPreview EventPreviewView-module_eventPreview"
>
<strong>
Image:
</strong>
city-map.png
</span>
</div>
`;
exports[`EventPreviewView > renders the default event preview 1`] = `
<div>
<span
class="mx_EventPreview EventPreviewView-module_eventPreview"
>
A short text message preview
</span>
</div>
`;
@@ -0,0 +1,8 @@
/*
* 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 { EventPreviewView, type EventPreviewViewModel, type EventPreviewViewSnapshot } from "./EventPreviewView";