Refactor and move MVideoBody to shared components (#32849)

* init MVideoBody to shared components, including test, stories and view

* fix prettier and other warnings

* move video message body to shared view + app viewmodel

* Fix prettier warnings and masking spinner for tests

* stabilize VideoBodyView screenshots with local media asset

* Disable spinner from changing image all the time

* Added mask over video spinner to prevent issues with new generated images on playwright tests

* Update prettier fix

* Update snapshot

* Add tests to cover different states of Video

* Update code to prevent the previous component Hack fix regarding jumps on the timeline.

* Update snapshot

* Update code to improve code quality for Sonar + update snapshot

* adde documentation snippets

* refactor: move m.video rendering into body factory

* docs: add tsdoc for video body view model

* docs: add thumbnail tsdoc for video body view model

* docs: add content-url tsdoc for video body view model

* docs: add dimensions tsdoc for video body view model

* docs: add aspect-ratio tsdoc for video body view model

* docs: add tsdoc for video body view state

* refactor: replace video body view state enum

* refactor: remove duplicate video body state init

* refactor: drop unused video body view state attribute

* Fix Prettier

* Update snapshot screenshot

* test: restore video story screenshot mask

* chore: refresh PR head

* Add mask to screenshot to pass CI tests

* test: narrow video story mask hook

* Fix easy Sonar warnings in video body components

* Move shared message body views into event-tile layout

* Move shared message body visual baselines

* Revert unrelated shared message body moves
This commit is contained in:
Zack
2026-04-01 09:48:22 +00:00
committed by GitHub
parent 3e04b24d1e
commit 0391543bbc
27 changed files with 1763 additions and 524 deletions
@@ -19,7 +19,11 @@ import {
} from "../../../../test-utils";
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { FileBodyFactory, renderMBody } from "../../../../../src/components/views/messages/MBodyFactory";
import {
FileBodyFactory,
VideoBodyFactory,
renderMBody,
} from "../../../../../src/components/views/messages/MBodyFactory";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
@@ -90,10 +94,14 @@ describe("MBodyFactory", () => {
expect(container).toMatchSnapshot();
});
it.each(["m.audio", "m.video", "m.text"])("returns null for unsupported msgtype %s", (msgtype) => {
it.each(["m.audio", "m.text"])("returns null for unsupported msgtype %s", (msgtype) => {
expect(renderMBody({ ...props, mxEvent: mkEvent(msgtype) })).toBeNull();
});
it("returns the video body factory for m.video", () => {
expect(renderMBody({ ...props, mxEvent: mkEvent("m.video") })?.type).toBe(VideoBodyFactory);
});
it("returns null when msgtype is missing", () => {
expect(renderMBody({ ...props, mxEvent: mkEvent() })).toBeNull();
});
@@ -116,7 +124,7 @@ describe("MBodyFactory", () => {
});
});
it.each(["m.file", "m.audio", "m.video"])(
it.each(["m.file", "m.audio"])(
"renderMBody fallback shows %s generic placeholder when showFileInfo is true",
async (msgtype) => {
const mediaEvent = new MatrixEvent({
@@ -23,7 +23,7 @@ import {
mockClientMethodsUser,
withClientContextRenderOptions,
} from "../../../../test-utils";
import MVideoBody from "../../../../../src/components/views/messages/MVideoBody";
import { VideoBodyFactory } from "../../../../../src/components/views/messages/MBodyFactory";
import type { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { MediaPreviewValue } from "../../../../../src/@types/media_preview";
@@ -33,7 +33,7 @@ jest.mock("matrix-encrypt-attachment", () => ({
decryptAttachment: jest.fn(),
}));
describe("MVideoBody", () => {
describe("VideoBodyFactory", () => {
const ourUserId = "@user:server";
const senderUserId = "@other_use:server";
const deviceId = "DEADB33F";
@@ -122,23 +122,25 @@ describe("MVideoBody", () => {
mediaEventHelper: { media: { isEncrypted: false } } as MediaEventHelper,
};
const { asFragment } = render(
const { container } = render(
<MatrixClientContext.Provider value={cli}>
<MVideoBody {...defaultProps} />
<VideoBodyFactory {...defaultProps} />
</MatrixClientContext.Provider>,
withClientContextRenderOptions(cli),
);
expect(asFragment()).toMatchSnapshot();
// If we get here, we did not crash.
expect(container.querySelector("video")).not.toBeNull();
});
it("should show poster for encrypted media before downloading it", async () => {
fetchMock.getOnce(thumbUrl, { status: 200 });
const { asFragment } = render(
<MVideoBody mxEvent={encryptedMediaEvent} mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)} />,
render(
<VideoBodyFactory
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
expect(asFragment()).toMatchSnapshot();
expect(await screen.findByLabelText("alt for a test video")).toHaveAttribute("poster");
});
describe("with video previews/thumbnails disabled", () => {
@@ -161,7 +163,7 @@ describe("MVideoBody", () => {
fetchMock.getOnce(thumbUrl, { status: 200 });
render(
<MVideoBody
<VideoBodyFactory
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
@@ -177,7 +179,7 @@ describe("MVideoBody", () => {
fetchMock.getOnce(thumbUrl, { status: 200 });
render(
<MVideoBody
<VideoBodyFactory
mxEvent={encryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
/>,
@@ -189,6 +191,7 @@ describe("MVideoBody", () => {
expect(placeholderButton).toBeInTheDocument();
fireEvent.click(placeholderButton);
await screen.findByLabelText("alt for a test video");
expect(fetchMock).toHaveFetched(thumbUrl);
});
@@ -214,16 +217,16 @@ describe("MVideoBody", () => {
},
},
});
const { asFragment } = render(
<MVideoBody
render(
<VideoBodyFactory
mxEvent={ourEncryptedMediaEvent}
mediaEventHelper={new MediaEventHelper(ourEncryptedMediaEvent)}
/>,
withClientContextRenderOptions(cli),
);
expect(await screen.findByLabelText("alt for a test video")).toBeInTheDocument();
expect(fetchMock).toHaveFetched(thumbUrl);
expect(asFragment()).toMatchSnapshot();
});
});
});
@@ -29,16 +29,12 @@ jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({
default: () => <div data-testid="image-body" />,
}));
jest.mock("../../../../../src/components/views/messages/MVideoBody", () => ({
__esModule: true,
default: () => <div data-testid="video-body" />,
}));
jest.mock("../../../../../src/components/views/messages/MBodyFactory", () => ({
__esModule: true,
DecryptionFailureBodyFactory: () => <div data-testid="decryption-failure-body" />,
FileBodyFactory: () => <div data-testid="file-body" />,
RedactedBodyFactory: () => <div className="mx_RedactedBody">Message deleted by Moderator</div>,
VideoBodyFactory: () => <video data-testid="video-body" />,
renderMBody: () => <div data-testid="file-body" />,
}));
@@ -47,6 +43,11 @@ jest.mock("../../../../../src/components/views/messages/MImageReplyBody", () =>
default: () => <div data-testid="image-reply-body" />,
}));
jest.mock("../../../../../src/hooks/useMediaVisible", () => ({
__esModule: true,
useMediaVisible: () => [true, jest.fn()],
}));
jest.mock("../../../../../src/components/views/messages/MStickerBody", () => ({
__esModule: true,
default: () => <div data-testid="sticker-body" />,
@@ -164,11 +165,11 @@ describe("MessageEvent", () => {
result.getByTestId("textual-body");
});
it("should render a TextualBody and an VideoBody", () => {
it("should render a TextualBody and a video element", () => {
event = createEvent("video/mp4", "video.mp4", MsgType.Video);
result = renderMessageEvent();
mockMedia();
result.getByTestId("video-body");
expect(result.container.querySelector("video")).not.toBeNull();
result.getByTestId("textual-body");
});
@@ -79,44 +79,6 @@ exports[`MBodyFactory renderMBody fallback shows m.file generic placeholder when
</div>
`;
exports[`MBodyFactory renderMBody fallback shows m.video generic placeholder when showFileInfo is true 1`] = `
<div>
<span
class="_content_f1s5h_8 mx_MFileBody"
>
<div
class="mx_MediaBody _mediaBody_rgndh_8"
data-type="info"
>
<button
aria-label="alt"
class="_button_13vu4_8 _has-icon_13vu4_60"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
<span>
alt
</span>
</button>
</div>
</span>
</div>
`;
exports[`MBodyFactory renderMBody renders download button for m.file in file rendering type 1`] = `
<div>
<span
@@ -1,78 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`MVideoBody does not crash when given portrait dimensions 1`] = `
<DocumentFragment>
<span
class="mx_MVideoBody"
>
<div
class="mx_MVideoBody_container"
style="max-width: 182px; max-height: 324px; aspect-ratio: 720/1280;"
>
<video
class="mx_MVideoBody"
controls=""
controlslist="nodownload"
crossorigin="anonymous"
poster="data:image/png;base64,00"
preload="none"
/>
<div
style="width: 182px; height: 324px;"
/>
</div>
</span>
</DocumentFragment>
`;
exports[`MVideoBody should show poster for encrypted media before downloading it 1`] = `
<DocumentFragment>
<span
class="mx_MVideoBody"
>
<div
class="mx_MVideoBody_container"
style="max-width: 40px; max-height: 50px; aspect-ratio: 40/50;"
>
<video
class="mx_MVideoBody"
controls=""
controlslist="nodownload"
crossorigin="anonymous"
poster="https://server/_matrix/media/v3/download/server/encrypted-poster"
preload="none"
title="alt for a test video"
/>
<div
style="width: 40px; height: 50px;"
/>
</div>
</span>
</DocumentFragment>
`;
exports[`MVideoBody with video previews/thumbnails disabled should download video if we were the sender 1`] = `
<DocumentFragment>
<span
class="mx_MVideoBody"
>
<div
class="mx_MVideoBody_container"
style="max-width: 40px; max-height: 50px; aspect-ratio: 40/50;"
>
<video
class="mx_MVideoBody"
controls=""
controlslist="nodownload"
crossorigin="anonymous"
poster="https://server/_matrix/media/v3/download/server/encrypted-poster"
preload="none"
title="alt for a test video"
/>
<div
style="width: 40px; height: 50px;"
/>
</div>
</span>
</DocumentFragment>
`;