Phase 2 : Refactor TextualBody to MVVM and remove legacy component (#33165)

* Refactor TextualBody to MVVM and remove legacy component

* Update snapshot + fix eslint warning

* update css to fix playwright tests failure

* return i18n into the MVVM

* Update snapshots

* Update tests to reflect the css changes

* Update snapshot

* Update css to correct letter-spacing

* Update css to fix playwright issues.

* Preserve inline emote sender rendering in TextualBodyView

* Update snapshot to reflect html change

* Update back to span instead of button, the default button css fails tests

* Extract TextualBodyFactory from MBodyFactory

* Update snapshot

* Update HTML snapshot to pass tests

* Update Snapshots

* Added several tests for coverage

* Remove double checks, merge function already checks.

* Remove unessecery comment

* revert to button

* Update snapshots because of the revert

* added Math.min() to simplify ternary expressions.

* Update playwright screenshots for accessibility

* Update playwright screenshots

* Update css to fix playwright fail

* Update screenshot + snapshots

* Add comments to props
This commit is contained in:
Zack
2026-04-28 09:07:19 +02:00
committed by GitHub
parent 7766ae92d7
commit 4e9655dc6b
42 changed files with 1061 additions and 555 deletions
@@ -164,7 +164,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta
contentContainer = (
<div className="mx_EventTile_content" ref={this.content}>
*&nbsp;
<span className="mx_MEmoteBody_sender">{name}</span>
<span className="mx_EditHistoryMessage_emoteSender">{name}</span>
&nbsp;{contentElements}
</div>
);
@@ -25,7 +25,6 @@ import UnknownBody from "./UnknownBody";
import { type IMediaBody } from "./IMediaBody";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import { type IBodyProps } from "./IBodyProps";
import TextualBody from "./TextualBody";
import MImageBody from "./MImageBody";
import MVoiceOrAudioBody from "./MVoiceOrAudioBody";
import MStickerBody from "./MStickerBody";
@@ -41,6 +40,7 @@ import {
VideoBodyFactory,
renderMBody,
} from "./MBodyFactory";
import { TextualBodyFactory } from "./TextualBodyFactory";
// onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
@@ -64,9 +64,9 @@ export interface IOperableEventTile {
}
const baseBodyTypes = new Map<string, React.ComponentType<IBodyProps>>([
[MsgType.Text, TextualBody],
[MsgType.Notice, TextualBody],
[MsgType.Emote, TextualBody],
[MsgType.Text, TextualBodyFactory],
[MsgType.Notice, TextualBodyFactory],
[MsgType.Emote, TextualBodyFactory],
[MsgType.Image, MImageBody],
[MsgType.File, (props: IBodyProps) => renderMBody(props, FileBodyFactory)!],
[MsgType.Audio, MVoiceOrAudioBody],
@@ -329,6 +329,6 @@ const CaptionBody: React.FunctionComponent<IBodyProps & { WrappedBodyType: React
}) => (
<div className="mx_EventTile_content">
<WrappedBodyType {...props} />
<TextualBody {...{ ...props, ref: undefined }} />
<TextualBodyFactory {...{ ...props, ref: undefined }} />
</div>
);
@@ -1,445 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
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, createRef, type SyntheticEvent, type MouseEvent, useCallback, useEffect } from "react";
import { MsgType } from "matrix-js-sdk/src/matrix";
import {
UrlPreviewGroupView,
type UrlPreview,
useCreateAutoDisposedViewModel,
EventContentBodyView,
LINKIFIED_DATA_ATTRIBUTE,
useViewModel,
} from "@element-hq/web-shared-components";
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
import { EventContentBodyViewModel } from "../../../viewmodels/message-body/EventContentBodyViewModel";
import { formatDate } from "../../../DateUtils";
import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks";
import { Action } from "../../../dispatcher/actions";
import QuestionDialog from "../dialogs/QuestionDialog";
import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
import EditMessageComposer from "../rooms/EditMessageComposer";
import { type IBodyProps } from "./IBodyProps";
import RoomContext from "../../../contexts/RoomContext";
import AccessibleButton from "../elements/AccessibleButton";
import { getParentEventId } from "../../../utils/Reply";
import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
import { type IEventTileOps } from "../rooms/EventTile";
import { UrlPreviewGroupViewModel } from "../../../viewmodels/message-body/UrlPreviewGroupViewModel.ts";
import { useMediaVisible } from "../../../hooks/useMediaVisible.ts";
import ImageView from "../elements/ImageView.tsx";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
import PosthogTrackers from "../../../PosthogTrackers.ts";
const logger = rootLogger.getChild("TextualBody");
type Props = IBodyProps & { urlPreviewViewModel: UrlPreviewGroupViewModel };
class InnerTextualBody extends React.Component<Props> {
private readonly contentRef = createRef<HTMLDivElement>();
public static contextType = RoomContext;
declare public context: React.ContextType<typeof RoomContext>;
private EventContentBodyViewModel: EventContentBodyViewModel;
public constructor(props: Props, context: React.ContextType<typeof RoomContext>) {
super(props, context);
const mxEvent = props.mxEvent;
const content = mxEvent.getContent();
const isEmote = content.msgtype === MsgType.Emote;
const willHaveWrapper =
!!props.replacingEventId || !!props.isSeeingThroughMessageHiddenForModeration || isEmote;
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);
this.EventContentBodyViewModel = new EventContentBodyViewModel({
as: willHaveWrapper ? "span" : "div",
includeDir: false,
mxEvent,
content,
stripReply,
linkify: true,
highlights: props.highlights,
renderTooltipsForAmbiguousLinks: true,
renderKeywordPills: true,
renderMentionPills: true,
renderCodeBlocks: true,
renderSpoilers: true,
client: context.room?.client ?? null,
});
}
public updateURLPreviewViewModel(): void {
const content = this.contentRef.current;
if (!content) {
return;
}
(async () => {
try {
void this.props.urlPreviewViewModel.updateEventElement(content);
} catch (ex) {
logger.warn("UrlPreviewViewModel failed to updateEventElement", ex);
}
})();
}
public componentDidUpdate(prevProps: Readonly<IBodyProps>): void {
// Update the ViewModel when relevant props change
const mxEventChanged = prevProps.mxEvent !== this.props.mxEvent;
const highlightsChanged = prevProps.highlights !== this.props.highlights;
const wrapperChanged =
prevProps.replacingEventId !== this.props.replacingEventId ||
prevProps.isSeeingThroughMessageHiddenForModeration !==
this.props.isSeeingThroughMessageHiddenForModeration;
if (mxEventChanged || highlightsChanged || wrapperChanged) {
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
const isEmote = content.msgtype === MsgType.Emote;
const willHaveWrapper =
!!this.props.replacingEventId || !!this.props.isSeeingThroughMessageHiddenForModeration || isEmote;
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);
this.EventContentBodyViewModel.setEventContent(mxEvent, content);
this.EventContentBodyViewModel.setStripReply(stripReply);
if (mxEventChanged || wrapperChanged) {
this.EventContentBodyViewModel.setAs(willHaveWrapper ? "span" : "div");
}
if (highlightsChanged) {
this.EventContentBodyViewModel.setHighlights(this.props.highlights);
}
}
this.updateURLPreviewViewModel();
}
public componentWillUnmount(): void {
this.EventContentBodyViewModel.dispose();
}
public shouldComponentUpdate(nextProps: Readonly<IBodyProps>): boolean {
// exploit that events are immutable :)
return (
nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.highlights !== this.props.highlights ||
nextProps.replacingEventId !== this.props.replacingEventId ||
nextProps.highlightLink !== this.props.highlightLink ||
nextProps.editState !== this.props.editState ||
nextProps.isSeeingThroughMessageHiddenForModeration !== this.props.isSeeingThroughMessageHiddenForModeration
);
}
private onEmoteSenderClick = (): void => {
const mxEvent = this.props.mxEvent;
dis.dispatch({
action: Action.ComposerInsert,
userId: mxEvent.getSender(),
timelineRenderingType: this.context.timelineRenderingType,
});
};
/**
* This acts as a fallback in-app navigation handler for any body links that
* were ignored as part of linkification because they were already links
* to start with (e.g. pills, links in the content).
*/
private onBodyLinkClick = (e: MouseEvent): void => {
let target: HTMLLinkElement | null = e.target as HTMLLinkElement;
// links processed by linkifyjs have their own handler so don't handle those here
if (target.dataset[LINKIFIED_DATA_ATTRIBUTE]) return;
if (target.nodeName !== "A") {
// Jump to parent as the `<a>` may contain children, e.g. an anchor wrapping an inline code section
target = target.closest<HTMLLinkElement>("a");
}
if (!target) return;
const localHref = tryTransformPermalinkToLocalHref(target.href);
if (localHref !== target.href) {
// it could be converted to a localHref -> therefore handle locally
e.preventDefault();
window.location.hash = localHref;
}
};
public getEventTileOps = (): IEventTileOps => ({
isWidgetHidden: () => {
// This controls whether the Show preview button is visibile.
return this.props.urlPreviewViewModel.isPreviewHiddenByUser;
},
unhideWidget: () => {
(async () => {
try {
await this.props.urlPreviewViewModel.onShowClick();
} catch (ex) {
logger.warn("UrlPreviewViewModel failed to onShowClick", ex);
}
})();
},
});
private onStarterLinkClick = (starterLink: string, ev: SyntheticEvent): void => {
ev.preventDefault();
// We need to add on our scalar token to the starter link, but we may not have one!
// In addition, we can't fetch one on click and then go to it immediately as that
// is then treated as a popup!
// We can get around this by fetching one now and showing a "confirmation dialog" (hurr hurr)
// which requires the user to click through and THEN we can open the link in a new tab because
// the window.open command occurs in the same stack frame as the onClick callback.
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
return;
}
// Go fetch a scalar token
const integrationManager = managers.getPrimaryManager();
const scalarClient = integrationManager?.getScalarClient();
scalarClient?.connect().then(() => {
const completeUrl = scalarClient.getStarterLink(starterLink);
const integrationsUrl = integrationManager!.uiUrl;
const { finished } = Modal.createDialog(QuestionDialog, {
title: _t("timeline|scalar_starter_link|dialog_title"),
description: (
<div>
{_t("timeline|scalar_starter_link|dialog_description", { integrationsUrl: integrationsUrl })}
</div>
),
button: _t("action|continue"),
});
finished.then(([confirmed]) => {
if (!confirmed) {
return;
}
const width = window.screen.width > 1024 ? 1024 : window.screen.width;
const height = window.screen.height > 800 ? 800 : window.screen.height;
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
const features = `height=${height}, width=${width}, top=${top}, left=${left},`;
const wnd = window.open(completeUrl, "_blank", features)!;
wnd.opener = null;
});
});
};
private openHistoryDialog = async (): Promise<void> => {
Modal.createDialog(MessageEditHistoryDialog, { mxEvent: this.props.mxEvent });
};
private renderEditedMarker(): JSX.Element {
const date = this.props.mxEvent.replacingEventDate();
const dateString = date && formatDate(date);
return (
<AccessibleButton
className="mx_EventTile_edited"
onClick={this.openHistoryDialog}
aria-label={_t("timeline|edits|tooltip_label", { date: dateString })}
title={_t("timeline|edits|tooltip_title", { date: dateString })}
caption={_t("timeline|edits|tooltip_sub")}
>
<span>{`(${_t("common|edited")})`}</span>
</AccessibleButton>
);
}
/**
* Render a marker informing the user that, while they can see the message,
* it is hidden for other users.
*/
private renderPendingModerationMarker(): JSX.Element {
let text;
const visibility = this.props.mxEvent.messageVisibility();
switch (visibility.visible) {
case true:
throw new Error("renderPendingModerationMarker should only be applied to hidden messages");
case false:
if (visibility.reason) {
text = _t("timeline|pending_moderation_reason", { reason: visibility.reason });
} else {
text = _t("timeline|pending_moderation");
}
break;
}
return <span className="mx_EventTile_pendingModeration">{`(${text})`}</span>;
}
public componentDidMount(): void {
this.updateURLPreviewViewModel();
}
public render(): React.ReactNode {
if (this.props.editState) {
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
return isWysiwygComposerEnabled ? (
<EditWysiwygComposer editorStateTransfer={this.props.editState} className="mx_EventTile_content" />
) : (
<EditMessageComposer editState={this.props.editState} className="mx_EventTile_content" />
);
}
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
const isNotice = content.msgtype === MsgType.Notice;
const isEmote = content.msgtype === MsgType.Emote;
const isCaption = [MsgType.Image, MsgType.File, MsgType.Audio, MsgType.Video].includes(
content.msgtype as MsgType,
);
const annotatedClassName = isEmote
? "mx_EventTile_annotated mx_EventTile_annotatedInline"
: "mx_EventTile_annotated";
const willHaveWrapper =
this.props.replacingEventId || this.props.isSeeingThroughMessageHiddenForModeration || isEmote;
let body = (
<EventContentBodyView
vm={this.EventContentBodyViewModel}
as={willHaveWrapper ? "span" : "div"}
ref={this.contentRef}
/>
);
if (this.props.replacingEventId) {
body = (
<div dir="auto" className={annotatedClassName}>
{body}
{this.renderEditedMarker()}
</div>
);
}
if (this.props.isSeeingThroughMessageHiddenForModeration) {
body = (
<div dir="auto" className={annotatedClassName}>
{body}
{this.renderPendingModerationMarker()}
</div>
);
}
if (this.props.highlightLink) {
body = <a href={this.props.highlightLink}>{body}</a>;
} else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") {
body = (
<AccessibleButton
kind="link_inline"
onClick={this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"])}
>
{body}
</AccessibleButton>
);
}
const urlPreviewWidget = <UrlPreviewGroupView vm={this.props.urlPreviewViewModel} />;
if (isEmote) {
return (
<div
id={this.props.id}
className="mx_MEmoteBody mx_EventTile_content"
onClick={this.onBodyLinkClick}
dir="auto"
>
*&nbsp;
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
</span>
&nbsp;
{body}
{urlPreviewWidget}
</div>
);
}
if (isNotice) {
return (
<div id={this.props.id} className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body}
{urlPreviewWidget}
</div>
);
}
if (isCaption) {
return (
<div id={this.props.id} className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
{body}
{urlPreviewWidget}
</div>
);
}
return (
<div id={this.props.id} className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body}
{urlPreviewWidget}
</div>
);
}
}
export default function TextualBody(props: IBodyProps): React.ReactElement {
const [mediaVisible] = useMediaVisible(props.mxEvent);
const client = useMatrixClientContext();
const onUrlPreviewImageClicked = useCallback((preview: UrlPreview): void => {
if (!preview.image?.imageFull) {
// Should never get this far, but doesn't hurt to check.
return;
}
const params = {
src: preview.image.imageFull,
width: preview.image.width,
height: preview.image.height,
name: preview.title,
fileSize: preview.image.fileSize,
link: preview.link,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
}, []);
const vm = useCreateAutoDisposedViewModel(
() =>
new UrlPreviewGroupViewModel({
client,
mxEvent: props.mxEvent,
mediaVisible: mediaVisible,
onImageClicked: onUrlPreviewImageClicked,
visible: props.showUrlPreview ?? false,
}),
);
useEffect(() => {
(async () => {
try {
await vm.updateHidden(props.showUrlPreview ?? false, mediaVisible);
} catch (ex) {
logger.warn("UrlPreviewViewModel failed to updateHidden", ex);
}
})();
}, [vm, props.showUrlPreview, mediaVisible]);
const { previews } = useViewModel(vm);
useEffect(() => {
if (previews.length === 0) {
return;
}
PosthogTrackers.instance.trackUrlPreview(props.mxEvent.getId()!, props.mxEvent.isEncrypted(), previews);
}, [props.mxEvent, previews]);
return <InnerTextualBody urlPreviewViewModel={vm} {...props} />;
}
@@ -0,0 +1,217 @@
/*
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, useContext, useEffect, useRef } from "react";
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
import { MsgType } from "matrix-js-sdk/src/matrix";
import {
EventContentBodyView,
TextualBodyView,
type TextualBodyContentElement,
type UrlPreview,
UrlPreviewGroupView,
useCreateAutoDisposedViewModel,
useViewModel,
} from "@element-hq/web-shared-components";
import { type IBodyProps } from "./IBodyProps";
import RoomContext from "../../../contexts/RoomContext";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useMediaVisible } from "../../../hooks/useMediaVisible";
import { TextualBodyViewModel } from "../../../viewmodels/room/timeline/event-tile/body/TextualBodyViewModel";
import { EventContentBodyViewModel } from "../../../viewmodels/message-body/EventContentBodyViewModel";
import { UrlPreviewGroupViewModel } from "../../../viewmodels/message-body/UrlPreviewGroupViewModel";
import { getParentEventId } from "../../../utils/Reply";
import Modal from "../../../Modal";
import SettingsStore from "../../../settings/SettingsStore";
import PosthogTrackers from "../../../PosthogTrackers";
import ImageView from "../elements/ImageView";
import EditMessageComposer from "../rooms/EditMessageComposer";
import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
const logger = rootLogger.getChild("TextualBodyFactory");
function getTextualBodyClassName(msgtype: MsgType | undefined): string {
if (msgtype === MsgType.Notice) {
return "mx_MNoticeBody mx_EventTile_content";
}
if (msgtype === MsgType.Emote) {
return "mx_MEmoteBody mx_EventTile_content";
}
if ([MsgType.Image, MsgType.File, MsgType.Audio, MsgType.Video].includes(msgtype as MsgType)) {
return "mx_MTextBody mx_EventTile_caption";
}
return "mx_MTextBody mx_EventTile_content";
}
export function TextualBodyFactory(props: Readonly<IBodyProps>): JSX.Element {
const roomContext = useContext(RoomContext);
const client = useMatrixClientContext();
const [mediaVisible] = useMediaVisible(props.mxEvent);
const content = props.mxEvent.getContent();
const isEmote = content.msgtype === MsgType.Emote;
const willHaveWrapper = !!props.replacingEventId || !!props.isSeeingThroughMessageHiddenForModeration || isEmote;
const stripReply = !props.mxEvent.replacingEvent() && !!getParentEventId(props.mxEvent);
const contentRef = useRef<TextualBodyContentElement>(null);
const textualBodyVm = useCreateAutoDisposedViewModel(
() =>
new TextualBodyViewModel({
id: props.id,
mxEvent: props.mxEvent,
highlightLink: props.highlightLink,
replacingEventId: props.replacingEventId,
isSeeingThroughMessageHiddenForModeration: props.isSeeingThroughMessageHiddenForModeration,
timelineRenderingType: roomContext.timelineRenderingType,
}),
);
const eventContentBodyVm = useCreateAutoDisposedViewModel(
() =>
new EventContentBodyViewModel({
as: willHaveWrapper ? "span" : "div",
includeDir: false,
mxEvent: props.mxEvent,
content,
stripReply,
linkify: true,
highlights: props.highlights,
renderTooltipsForAmbiguousLinks: true,
renderKeywordPills: true,
renderMentionPills: true,
renderCodeBlocks: true,
renderSpoilers: true,
client: roomContext.room?.client ?? client ?? null,
}),
);
const urlPreviewVm = useCreateAutoDisposedViewModel(
() =>
new UrlPreviewGroupViewModel({
client,
mxEvent: props.mxEvent,
mediaVisible,
onImageClicked: (preview: UrlPreview): void => {
if (!preview.image?.imageFull) {
return;
}
Modal.createDialog(
ImageView,
{
src: preview.image.imageFull,
width: preview.image.width,
height: preview.image.height,
name: preview.title,
fileSize: preview.image.fileSize,
link: preview.link,
},
"mx_Dialog_lightbox",
undefined,
true,
);
},
visible: props.showUrlPreview ?? false,
}),
);
const { previews } = useViewModel(urlPreviewVm);
useEffect(() => {
textualBodyVm.setId(props.id);
}, [props.id, textualBodyVm]);
useEffect(() => {
textualBodyVm.setEvent(props.mxEvent);
}, [props.mxEvent, textualBodyVm]);
useEffect(() => {
textualBodyVm.setHighlightLink(props.highlightLink);
}, [props.highlightLink, textualBodyVm]);
useEffect(() => {
textualBodyVm.setReplacingEventId(props.replacingEventId);
}, [props.replacingEventId, textualBodyVm]);
useEffect(() => {
textualBodyVm.setIsSeeingThroughMessageHiddenForModeration(props.isSeeingThroughMessageHiddenForModeration);
}, [props.isSeeingThroughMessageHiddenForModeration, textualBodyVm]);
useEffect(() => {
textualBodyVm.setTimelineRenderingType(roomContext.timelineRenderingType);
}, [roomContext.timelineRenderingType, textualBodyVm]);
useEffect(() => {
eventContentBodyVm.setEventContent(props.mxEvent, content);
}, [content, props.mxEvent, eventContentBodyVm]);
useEffect(() => {
eventContentBodyVm.setStripReply(stripReply);
}, [stripReply, eventContentBodyVm]);
useEffect(() => {
eventContentBodyVm.setAs(willHaveWrapper ? "span" : "div");
}, [willHaveWrapper, eventContentBodyVm]);
useEffect(() => {
eventContentBodyVm.setHighlights(props.highlights);
}, [props.highlights, eventContentBodyVm]);
useEffect(() => {
const eventElement = contentRef.current;
if (!eventElement) {
return;
}
void urlPreviewVm.updateEventElement(eventElement).catch((error) => {
logger.warn("UrlPreviewViewModel failed to updateEventElement", error);
});
}, [
props.mxEvent,
props.highlights,
props.replacingEventId,
props.isSeeingThroughMessageHiddenForModeration,
urlPreviewVm,
]);
useEffect(() => {
void urlPreviewVm.updateHidden(props.showUrlPreview ?? false, mediaVisible).catch((error) => {
logger.warn("UrlPreviewViewModel failed to updateHidden", error);
});
}, [props.showUrlPreview, mediaVisible, urlPreviewVm]);
useEffect(() => {
if (previews.length === 0) {
return;
}
PosthogTrackers.instance.trackUrlPreview(props.mxEvent.getId()!, props.mxEvent.isEncrypted(), previews);
}, [props.mxEvent, previews]);
if (props.editState) {
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
return isWysiwygComposerEnabled ? (
<EditWysiwygComposer editorStateTransfer={props.editState} className="mx_EventTile_content" />
) : (
<EditMessageComposer editState={props.editState} className="mx_EventTile_content" />
);
}
return (
<TextualBodyView
vm={textualBodyVm}
body={<EventContentBodyView vm={eventContentBodyVm} as={willHaveWrapper ? "span" : "div"} />}
bodyRef={contentRef}
urlPreviews={<UrlPreviewGroupView vm={urlPreviewVm} />}
className={getTextualBodyClassName(content.msgtype as MsgType | undefined)}
/>
);
}