Files
ThreadNet-Web/apps/web/src/components/views/messages/MBodyFactory.tsx
T
ZackandGitHub 0391543bbc 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
2026-04-01 09:48:22 +00:00

181 lines
6.2 KiB
TypeScript

/*
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, type RefObject, useContext, useEffect, useRef } from "react";
import { MsgType } from "matrix-js-sdk/src/matrix";
import {
DecryptionFailureBodyView,
FileBodyView,
RedactedBodyView,
VideoBodyView,
useCreateAutoDisposedViewModel,
} from "@element-hq/web-shared-components";
import { type IBodyProps } from "./IBodyProps";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import { LocalDeviceVerificationStateContext } from "../../../contexts/LocalDeviceVerificationStateContext";
import { useMediaVisible } from "../../../hooks/useMediaVisible";
import { DecryptionFailureBodyViewModel } from "../../../viewmodels/room/timeline/event-tile/body/DecryptionFailureBodyViewModel";
import { FileBodyViewModel } from "../../../viewmodels/message-body/FileBodyViewModel";
import { RedactedBodyViewModel } from "../../../viewmodels/message-body/RedactedBodyViewModel";
import { VideoBodyViewModel } from "../../../viewmodels/message-body/VideoBodyViewModel";
type MBodyComponent = React.ComponentType<IBodyProps>;
export function FileBodyFactory({
mxEvent,
mediaEventHelper,
forExport,
showFileInfo,
}: Pick<IBodyProps, "mxEvent" | "mediaEventHelper" | "forExport" | "showFileInfo">): JSX.Element {
const { timelineRenderingType } = useContext(RoomContext);
const refIFrame = useRef<HTMLIFrameElement>(null) as RefObject<HTMLIFrameElement>;
const refLink = useRef<HTMLAnchorElement>(null) as RefObject<HTMLAnchorElement>;
const vm = useCreateAutoDisposedViewModel(
() =>
new FileBodyViewModel({
mxEvent,
mediaEventHelper,
forExport,
showFileInfo,
timelineRenderingType,
refIFrame,
refLink,
}),
);
useEffect(() => {
vm.setProps({
mxEvent,
mediaEventHelper,
forExport,
showFileInfo,
timelineRenderingType,
});
}, [mxEvent, mediaEventHelper, forExport, showFileInfo, timelineRenderingType, vm]);
return <FileBodyView vm={vm} refIFrame={refIFrame} refLink={refLink} className="mx_MFileBody" />;
}
export function VideoBodyFactory({
mxEvent,
mediaEventHelper,
forExport,
inhibitInteraction,
}: Readonly<Pick<IBodyProps, "mxEvent" | "mediaEventHelper" | "forExport" | "inhibitInteraction">>): JSX.Element {
const { timelineRenderingType } = useContext(RoomContext);
const [mediaVisible, setMediaVisible] = useMediaVisible(mxEvent);
const videoRef = useRef<HTMLVideoElement>(null);
const vm = useCreateAutoDisposedViewModel(
() =>
new VideoBodyViewModel({
mxEvent,
mediaEventHelper,
forExport,
inhibitInteraction,
mediaVisible,
onPreviewClick: (): void => setMediaVisible(true),
videoRef,
}),
);
useEffect(() => {
vm.loadInitialMediaIfVisible();
}, [vm]);
useEffect(() => {
vm.setEvent(mxEvent, mediaEventHelper);
}, [mxEvent, mediaEventHelper, vm]);
useEffect(() => {
vm.setForExport(forExport);
}, [forExport, vm]);
useEffect(() => {
vm.setInhibitInteraction(inhibitInteraction);
}, [inhibitInteraction, vm]);
useEffect(() => {
vm.setMediaVisible(mediaVisible);
}, [mediaVisible, vm]);
useEffect(() => {
vm.setOnPreviewClick((): void => setMediaVisible(true));
}, [setMediaVisible, vm]);
const showFileBody =
!forExport &&
timelineRenderingType !== TimelineRenderingType.Room &&
timelineRenderingType !== TimelineRenderingType.Pinned &&
timelineRenderingType !== TimelineRenderingType.Search;
return (
<VideoBodyView
vm={vm}
className="mx_MVideoBody"
containerClassName="mx_MVideoBody_container"
videoRef={videoRef}
>
{showFileBody ? (
<FileBodyFactory
mxEvent={mxEvent}
mediaEventHelper={mediaEventHelper}
forExport={forExport}
showFileInfo={false}
/>
) : null}
</VideoBodyView>
);
}
export function RedactedBodyFactory({ mxEvent, ref }: Pick<IBodyProps, "mxEvent" | "ref">): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new RedactedBodyViewModel({ mxEvent }));
useEffect(() => {
vm.setEvent(mxEvent);
}, [mxEvent, vm]);
return <RedactedBodyView vm={vm} ref={ref} className="mx_RedactedBody" />;
}
export function DecryptionFailureBodyFactory({ mxEvent, ref }: Pick<IBodyProps, "mxEvent" | "ref">): JSX.Element {
const verificationState = useContext(LocalDeviceVerificationStateContext);
const vm = useCreateAutoDisposedViewModel(
() =>
new DecryptionFailureBodyViewModel({
decryptionFailureCode: mxEvent.decryptionFailureReason,
verificationState,
}),
);
useEffect(() => {
vm.setDecryptionFailureCode(mxEvent.decryptionFailureReason);
vm.setVerificationState(verificationState);
}, [mxEvent, verificationState, vm]);
return <DecryptionFailureBodyView vm={vm} ref={ref} className="mx_DecryptionFailureBody mx_EventTile_content" />;
}
// Message body factory registry for bodies that already route through view-model-backed wrappers.
const MESSAGE_BODY_TYPES = new Map<string, MBodyComponent>([
[MsgType.File, FileBodyFactory],
[MsgType.Video, VideoBodyFactory],
]);
// Render a body using the picked factory.
// Falls back to the provided factory when msgtype has no specific handler.
export function renderMBody(props: IBodyProps, fallbackFactory?: MBodyComponent): JSX.Element | null {
const BodyType = MESSAGE_BODY_TYPES.get(props.mxEvent.getContent().msgtype as string) ?? fallbackFactory;
if (!BodyType) {
return null;
}
return <BodyType {...props} />;
}