From 8d076c897d1a8f2510153114373d16ee06c7388c Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 9 Mar 2026 10:58:05 +0100 Subject: [PATCH] Refactor EventContentBody to shared-components (#31914) * Init of refactoring of eventcontentbody * update stories css by copying css from element x to shared components * Replaced old component EventContentBody with newly created mmvm component EventContentBodyViewModel * Refactor TextualBody and EditHistoryMessage to properly manage EventContentBodyViewModel * generated snapshot after vitest * Update import placement for eslint to pass CI * Fixed lint warnings * Update css for codeblock to represent js highlight * test: add EventContentBodyViewModel snapshot coverage * fix: pass content ref to EventContentBodyView for link previews * Fix: return to old code that passed tests * Added storybook snapshots * Removal of old component that is being unused * Update snapshot * Fix missing enableBigEmoji and shouldShowPillAvatar settings in EventContentBodyViewModel * update snapshot * narrow setProps to mutable fields and skip no-op snapshot recomputes * Update Snapshots * replace EventContentBodyViewModel setProps with explicit setters and update call sites * render body in view and keep parser/replacer in snapshot * Eslint Restruct * Eslint Restructure * Removed unused function, moved to shared component * Remove Unused Module (Moved To Shared Component) * Disable EventContent-body Test to check weather it fixes CI * Enable EventContentBody Tests * Remove EventTest * Update Include in Vitest * Added EventContentBody test * Update Package.json * Update Lockfile * Update dependencies * update lockfile * ptimize EventContentBodyViewModel to recompute/merge only changed snapshot fields * Update snapshots * setEventContent and setStripReply run whenever the existing update block runs * defined arrow functions for undefined runtime issues that might occur. * Update test cases * Update packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx Co-authored-by: R Midhun Suresh * Update packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx Co-authored-by: R Midhun Suresh * move big-emoji and pill-avatar setting watchers into EventContentBodyViewModel * Update packages/shared-components/src/message-body/EventContentBody/index.tsx Co-authored-by: Florian Duros * Update packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx Co-authored-by: Florian Duros * Update packages/shared-components/src/message-body/EventContentBody/EventContentBody.test.tsx Co-authored-by: Florian Duros * Update packages/shared-components/src/message-body/EventContentBody/EventContentBody.stories.tsx Co-authored-by: Florian Duros * Update packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx Co-authored-by: Florian Duros * Update packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx Co-authored-by: Florian Duros * Fix dubblicate variables * clarify applyReplacerOnString input/replacer params * Added memo to the view * Prettier Fix * Update apps/web/src/viewmodels/message-body/EventContentBodyViewModel.ts Co-authored-by: Florian Duros * Added compund variables instead of reguler values * Added boolean default values * remove redundant setting props from TextualBody and EditHistoryMessage * Prettier FIx * replace MatrixClientPeg usage with `client: MatrixClient | null` passed from context * TextualBody now passes EventContentBodyViewModel `client` from RoomContext. * Remove redundant as prop from EventContentBody VM usage * Normalize EventContentBodyViewModel renderer flags to booleans --------- Co-authored-by: R Midhun Suresh Co-authored-by: Florian Duros --- .../views/messages/EditHistoryMessage.tsx | 43 +- .../views/messages/EventContentBody.tsx | 189 --------- .../components/views/messages/TextualBody.tsx | 92 ++++- apps/web/src/renderer/index.ts | 2 +- apps/web/src/renderer/utils.tsx | 24 +- .../message-body/EventContentBodyViewModel.ts | 292 ++++++++++++++ .../views/messages/TextualBody-test.tsx | 15 +- .../EventContentBodyViewModel-test.tsx | 373 ++++++++++++++++++ .../as-span-auto.png | Bin 0 -> 20247 bytes .../big-emoji-auto.png | Bin 0 -> 24216 bytes .../code-block-auto.png | Bin 0 -> 22875 bytes .../html-content-auto.png | Bin 0 -> 21025 bytes .../plain-text-auto.png | Bin 0 -> 20467 bytes .../with-highlight-auto.png | Bin 0 -> 21503 bytes packages/shared-components/package.json | 1 + packages/shared-components/src/index.ts | 1 + .../EventContentBody.module.css | 89 +++++ .../EventContentBody.stories.tsx | 92 +++++ .../EventContentBody.test.tsx | 53 +++ .../EventContentBody/EventContentBodyView.tsx | 110 ++++++ .../EventContentBody.test.tsx.snap | 106 +++++ .../message-body/EventContentBody/index.tsx | 13 + .../src/utils/applyReplacerOnString.tsx | 35 ++ pnpm-lock.yaml | 3 + 24 files changed, 1281 insertions(+), 252 deletions(-) delete mode 100644 apps/web/src/components/views/messages/EventContentBody.tsx create mode 100644 apps/web/src/viewmodels/message-body/EventContentBodyViewModel.ts create mode 100644 apps/web/test/viewmodels/message-body/EventContentBodyViewModel-test.tsx create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/as-span-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/big-emoji-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/code-block-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/html-content-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/plain-text-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/with-highlight-auto.png create mode 100644 packages/shared-components/src/message-body/EventContentBody/EventContentBody.module.css create mode 100644 packages/shared-components/src/message-body/EventContentBody/EventContentBody.stories.tsx create mode 100644 packages/shared-components/src/message-body/EventContentBody/EventContentBody.test.tsx create mode 100644 packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx create mode 100644 packages/shared-components/src/message-body/EventContentBody/__snapshots__/EventContentBody.test.tsx.snap create mode 100644 packages/shared-components/src/message-body/EventContentBody/index.tsx create mode 100644 packages/shared-components/src/utils/applyReplacerOnString.tsx diff --git a/apps/web/src/components/views/messages/EditHistoryMessage.tsx b/apps/web/src/components/views/messages/EditHistoryMessage.tsx index c025cac9a5..4629d4786e 100644 --- a/apps/web/src/components/views/messages/EditHistoryMessage.tsx +++ b/apps/web/src/components/views/messages/EditHistoryMessage.tsx @@ -9,8 +9,9 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, createRef } from "react"; import { type EventStatus, type IContent, type MatrixEvent, MatrixEventEvent, MsgType } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; +import { EventContentBodyView } from "@element-hq/web-shared-components"; -import EventContentBody from "./EventContentBody.tsx"; +import { EventContentBodyViewModel } from "../../../viewmodels/message-body/EventContentBodyViewModel"; import { editBodyDiffToHtml } from "../../../utils/MessageDiffUtils"; import { formatTime } from "../../../DateUtils"; import { _t } from "../../../languageHandler"; @@ -45,17 +46,39 @@ export default class EditHistoryMessage extends React.PureComponent; private content = createRef(); + private EventContentBodyViewModel: EventContentBodyViewModel; public constructor(props: IProps, context: React.ContextType) { super(props, context); const cli = this.context; const userId = cli.getSafeUserId(); - const event = this.props.mxEvent; + const event = props.mxEvent; const room = cli.getRoom(event.getRoomId()); event.localRedactionEvent()?.on(MatrixEventEvent.Status, this.onAssociatedStatusChanged); const canRedact = room?.currentState.maySendRedactionForEvent(event, userId) ?? false; this.state = { canRedact, sendStatus: event.getAssociatedStatus() }; + + const mxEventContent = getReplacedContent(event); + this.EventContentBodyViewModel = new EventContentBodyViewModel({ + mxEvent: event, + content: mxEventContent, + highlights: [], + stripReply: true, + renderTooltipsForAmbiguousLinks: true, + renderMentionPills: true, + renderCodeBlocks: true, + renderSpoilers: true, + linkify: true, + client: cli, + }); + } + + public componentDidUpdate(prevProps: IProps): void { + if (prevProps.mxEvent !== this.props.mxEvent) { + const mxEventContent = getReplacedContent(this.props.mxEvent); + this.EventContentBodyViewModel.setEventContent(this.props.mxEvent, mxEventContent); + } } private onAssociatedStatusChanged = (): void => { @@ -92,6 +115,7 @@ export default class EditHistoryMessage extends React.PureComponent - ); + contentElements = ; } if (mxEvent.getContent().msgtype === MsgType.Emote) { const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); diff --git a/apps/web/src/components/views/messages/EventContentBody.tsx b/apps/web/src/components/views/messages/EventContentBody.tsx deleted file mode 100644 index 50d95642f8..0000000000 --- a/apps/web/src/components/views/messages/EventContentBody.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/* -Copyright 2025 New Vector 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, { memo, useContext, useMemo, type Ref } from "react"; -import { type IContent, type MatrixEvent, MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix"; -import parse from "html-react-parser"; -import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; - -import { bodyToNode } from "../../../HtmlUtils.tsx"; -import PlatformPeg from "../../../PlatformPeg.ts"; -import { - applyReplacerOnString, - combineRenderers, - type Replacer, - type RendererMap, - keywordPillRenderer, - mentionPillRenderer, - ambiguousLinkTooltipRenderer, - codeBlockRenderer, - spoilerRenderer, -} from "../../../renderer"; -import MatrixClientContext from "../../../contexts/MatrixClientContext.tsx"; -import { useSettingValue } from "../../../hooks/useSettings.ts"; -import { filterBoolean } from "../../../utils/arrays.ts"; -import { useMediaVisible } from "../../../hooks/useMediaVisible.ts"; - -/** - * Returns a RegExp pattern for the keyword in the push rule of the given Matrix event, if any - * @param mxEvent - the Matrix event to get the push rule keyword pattern from - */ -const getPushDetailsKeywordPatternRegexp = (mxEvent: MatrixEvent): RegExp | undefined => { - const pushDetails = mxEvent.getPushDetails(); - if ( - pushDetails.rule?.enabled && - pushDetails.rule.kind === PushRuleKind.ContentSpecific && - pushDetails.rule.pattern - ) { - return PushProcessor.getPushRuleGlobRegex(pushDetails.rule.pattern, true, "gi"); - } - return undefined; -}; - -interface ReplacerOptions { - /** - * Whether to render room/user mentions as pills - */ - renderMentionPills?: boolean; - /** - * Whether to render push rule keywords as pills - */ - renderKeywordPills?: boolean; - /** - * Whether to render spoilers as hidden content revealed on click - */ - renderSpoilers?: boolean; - /** - * Whether to render code blocks as syntax highlighted code with a copy to clipboard button - */ - renderCodeBlocks?: boolean; - /** - * Whether to render tooltips for ambiguous links, only effective on platforms which specify `needsUrlTooltips` true - */ - renderTooltipsForAmbiguousLinks?: boolean; -} - -// Returns a memoized Replacer based on the input parameters -const useReplacer = (content: IContent, mxEvent: MatrixEvent | undefined, options: ReplacerOptions): Replacer => { - const cli = useContext(MatrixClientContext); - const room = cli.getRoom(mxEvent?.getRoomId()) ?? undefined; - - const shouldShowPillAvatar = useSettingValue("Pill.shouldShowPillAvatar"); - const isHtml = content.format === "org.matrix.custom.html"; - - const replacer = useMemo(() => { - const keywordRegexpPattern = mxEvent ? getPushDetailsKeywordPatternRegexp(mxEvent) : undefined; - const replacers = filterBoolean([ - options.renderMentionPills ? mentionPillRenderer : undefined, - options.renderKeywordPills && keywordRegexpPattern ? keywordPillRenderer : undefined, - options.renderTooltipsForAmbiguousLinks && PlatformPeg.get()?.needsUrlTooltips() - ? ambiguousLinkTooltipRenderer - : undefined, - options.renderSpoilers ? spoilerRenderer : undefined, - options.renderCodeBlocks ? codeBlockRenderer : undefined, - ]); - return combineRenderers(...replacers)({ - isHtml, - mxEvent, - room, - shouldShowPillAvatar, - keywordRegexpPattern, - }); - }, [ - mxEvent, - options.renderMentionPills, - options.renderKeywordPills, - options.renderTooltipsForAmbiguousLinks, - options.renderSpoilers, - options.renderCodeBlocks, - isHtml, - room, - shouldShowPillAvatar, - ]); - - return replacer; -}; - -interface Props extends ReplacerOptions { - /** - * Whether to render the content in a div or span - */ - as: "span" | "div"; - /** - * Whether to render links as clickable anchors - */ - linkify: boolean; - /** - * The Matrix event to render, required for renderMentionPills & renderKeywordPills - */ - mxEvent?: MatrixEvent; - /** - * The content to render - */ - content: IContent; - /** - * Whether to strip reply fallbacks from the content before rendering - */ - stripReply?: boolean; - /** - * Highlights to emphasise in the content - */ - highlights?: string[]; - /** - * Whether to include the `dir="auto"` attribute on the rendered element - */ - includeDir?: boolean; - ref?: Ref; -} - -/** - * Component to render a Matrix event's content body. - * If the content is formatted HTML then it will be sanitised before rendering. - * A number of rendering features are supported as configured by {@link ReplacerOptions} - * Returns a div or span depending on `as`, the `dir` on a `div` is always set to `"auto"` but set by `includeDir` otherwise. - */ -const EventContentBody = memo( - ({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => { - const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji"); - const [mediaIsVisible] = useMediaVisible(mxEvent); - - const replacer = useReplacer(content, mxEvent, options); - - const isEmote = content.msgtype === MsgType.Emote; - - const { strippedBody, formattedBody, emojiBodyElements, className } = useMemo( - () => - bodyToNode(content, highlights, { - disableBigEmoji: isEmote || !enableBigEmoji, - // Part of Replies fallback support - stripReplyFallback: stripReply, - mediaIsVisible, - linkify, - }), - [content, mediaIsVisible, enableBigEmoji, highlights, isEmote, stripReply, linkify], - ); - - if (as === "div") includeDir = true; // force dir="auto" on divs - - const As = as; - const body = formattedBody ? ( - - {parse(formattedBody, { - replace: replacer, - })} - - ) : ( - - {applyReplacerOnString(emojiBodyElements || strippedBody, replacer)} - - ); - - return body; - }, -); - -export default EventContentBody; diff --git a/apps/web/src/components/views/messages/TextualBody.tsx b/apps/web/src/components/views/messages/TextualBody.tsx index 34d6e9be94..5e65dc3271 100644 --- a/apps/web/src/components/views/messages/TextualBody.tsx +++ b/apps/web/src/components/views/messages/TextualBody.tsx @@ -8,8 +8,9 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, createRef, type SyntheticEvent, type MouseEvent } from "react"; import { MsgType } from "matrix-js-sdk/src/matrix"; +import { EventContentBodyView } from "@element-hq/web-shared-components"; -import EventContentBody from "./EventContentBody.tsx"; +import { EventContentBodyViewModel } from "../../../viewmodels/message-body/EventContentBodyViewModel"; import { formatDate } from "../../../DateUtils"; import Modal from "../../../Modal"; import dis from "../../../dispatcher/dispatcher"; @@ -44,22 +45,77 @@ export default class TextualBody extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; + private EventContentBodyViewModel: EventContentBodyViewModel; + public state = { links: [], widgetHidden: false, }; + public constructor(props: IBodyProps, context: React.ContextType) { + 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 componentDidMount(): void { if (!this.props.editState) { this.applyFormatting(); } } - private applyFormatting(): void { - this.calculateUrlPreview(); - } - public componentDidUpdate(prevProps: Readonly): 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); + } + } + + // Handle formatting updates if (!this.props.editState) { const stoppedEditing = prevProps.editState && !this.props.editState; const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; @@ -70,6 +126,14 @@ export default class TextualBody extends React.Component { } } + public componentWillUnmount(): void { + this.EventContentBodyViewModel.dispose(); + } + + private applyFormatting(): void { + this.calculateUrlPreview(); + } + public shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean { //console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); @@ -311,6 +375,7 @@ export default class TextualBody extends React.Component { ); } + const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); const isNotice = content.msgtype === MsgType.Notice; @@ -321,23 +386,12 @@ export default class TextualBody extends React.Component { 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); + let body = ( - ); diff --git a/apps/web/src/renderer/index.ts b/apps/web/src/renderer/index.ts index 3de73c8bbe..92e8bc12b0 100644 --- a/apps/web/src/renderer/index.ts +++ b/apps/web/src/renderer/index.ts @@ -9,4 +9,4 @@ export { ambiguousLinkTooltipRenderer } from "./link-tooltip"; export { keywordPillRenderer, mentionPillRenderer } from "./pill"; export { spoilerRenderer } from "./spoiler"; export { codeBlockRenderer } from "./code-block"; -export { applyReplacerOnString, combineRenderers, type RendererMap, type Replacer } from "./utils"; +export { combineRenderers, type RendererMap, type Replacer } from "./utils"; diff --git a/apps/web/src/renderer/utils.tsx b/apps/web/src/renderer/utils.tsx index 3109e9b447..4ebbd0b365 100644 --- a/apps/web/src/renderer/utils.tsx +++ b/apps/web/src/renderer/utils.tsx @@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX } from "react"; -import { type DOMNode, Element, type HTMLReactParserOptions, Text } from "html-react-parser"; +import { type JSX } from "react"; +import { type DOMNode, Element, type HTMLReactParserOptions, type Text } from "html-react-parser"; import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix"; /** @@ -44,26 +44,6 @@ export const hasParentMatching = (node: Element, matcher: (node: ParentNode | nu */ export type Replacer = HTMLReactParserOptions["replace"]; -/** - * Passes through any non-string inputs verbatim, as such they should only be used for emoji bodies - */ -export function applyReplacerOnString( - input: string | JSX.Element[], - replacer: Replacer, -): JSX.Element | JSX.Element[] | string { - if (!replacer) return input; - - const arr = Array.isArray(input) ? input : [input]; - return arr.map((input, index): JSX.Element => { - if (typeof input === "string") { - return ( - {(replacer(new Text(input), 0) as JSX.Element) || input} - ); - } - return input; - }); -} - interface Parameters { isHtml: boolean; replace: Replacer; diff --git a/apps/web/src/viewmodels/message-body/EventContentBodyViewModel.ts b/apps/web/src/viewmodels/message-body/EventContentBodyViewModel.ts new file mode 100644 index 0000000000..e1f765403f --- /dev/null +++ b/apps/web/src/viewmodels/message-body/EventContentBodyViewModel.ts @@ -0,0 +1,292 @@ +/* +Copyright 2025 New Vector 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 { type IContent, type MatrixClient, type MatrixEvent, MsgType, PushRuleKind } from "matrix-js-sdk/src/matrix"; +import parse from "html-react-parser"; +import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; +import { + BaseViewModel, + type EventContentBodyViewSnapshot, + type EventContentBodyViewModel as EventContentBodyViewModelInterface, +} from "@element-hq/web-shared-components"; + +import { bodyToNode } from "../../HtmlUtils"; +import PlatformPeg from "../../PlatformPeg"; +import { + combineRenderers, + type Replacer, + type RendererMap, + keywordPillRenderer, + mentionPillRenderer, + ambiguousLinkTooltipRenderer, + codeBlockRenderer, + spoilerRenderer, +} from "../../renderer"; +import { filterBoolean } from "../../utils/arrays"; +import SettingsStore from "../../settings/SettingsStore"; + +/** + * Options for configuring which renderers to apply. + */ +export interface ReplacerOptions { + /** + * Whether to render room/user mentions as pills. + * @default false + */ + renderMentionPills?: boolean; + /** + * Whether to render push rule keywords as pills. + * @default false + */ + renderKeywordPills?: boolean; + /** + * Whether to render spoilers as hidden content revealed on click. + * @default false + */ + renderSpoilers?: boolean; + /** + * Whether to render code blocks as syntax highlighted code with a copy to clipboard button. + * @default false + */ + renderCodeBlocks?: boolean; + /** + * Whether to render tooltips for ambiguous links, only effective on platforms which specify `needsUrlTooltips` true. + * @default false + */ + renderTooltipsForAmbiguousLinks?: boolean; +} + +/** + * Props for the EventContentBody ViewModel. + */ +export interface EventContentBodyViewModelProps extends ReplacerOptions { + /** + * The content to render. + */ + content: IContent; + /** + * The Matrix event to render, required for renderMentionPills & renderKeywordPills. + */ + mxEvent?: MatrixEvent; + /** + * Whether to strip reply fallbacks from the content before rendering. + * @default false + */ + stripReply?: boolean; + /** + * Highlights to emphasise in the content. + */ + highlights?: string[]; + /** + * Whether to render links as clickable anchors. + * @default true + */ + linkify?: boolean; + /** + * Whether to include the `dir="auto"` attribute on the rendered element. + * Always true for "div" elements. + * @default true + */ + includeDir?: boolean; + /** + * Whether to render the content in a div or span. + * @default "span" + */ + as?: "span" | "div"; + /** + * Whether big emoji should be enabled. + * @default false + */ + enableBigEmoji?: boolean; + /** + * Whether media is visible in the event. + * @default true + */ + mediaIsVisible?: boolean; + /** + * Whether to show pill avatars. + * @default true + */ + shouldShowPillAvatar?: boolean; + /** + * Matrix client used to resolve room context for renderers. + */ + client: MatrixClient | null; +} + +/** + * Returns a RegExp pattern for the keyword in the push rule of the given Matrix event, if any. + */ +const getPushDetailsKeywordPatternRegexp = (mxEvent: MatrixEvent): RegExp | undefined => { + const pushDetails = mxEvent.getPushDetails(); + if ( + pushDetails.rule?.enabled && + pushDetails.rule.kind === PushRuleKind.ContentSpecific && + pushDetails.rule.pattern + ) { + return PushProcessor.getPushRuleGlobRegex(pushDetails.rule.pattern, true, "gi"); + } + return undefined; +}; + +/** + * Creates a replacer function based on the provided options and context. + */ +const createReplacer = (props: EventContentBodyViewModelProps): Replacer => { + const { content, mxEvent, shouldShowPillAvatar, client, ...options } = props; + const room = client?.getRoom(mxEvent?.getRoomId()) ?? undefined; + const isHtml = content.format === "org.matrix.custom.html"; + const keywordRegexpPattern = mxEvent ? getPushDetailsKeywordPatternRegexp(mxEvent) : undefined; + + const replacers = filterBoolean([ + options.renderMentionPills ? mentionPillRenderer : undefined, + options.renderKeywordPills && keywordRegexpPattern ? keywordPillRenderer : undefined, + options.renderTooltipsForAmbiguousLinks && PlatformPeg.get()?.needsUrlTooltips() + ? ambiguousLinkTooltipRenderer + : undefined, + options.renderSpoilers ? spoilerRenderer : undefined, + options.renderCodeBlocks ? codeBlockRenderer : undefined, + ]); + + return combineRenderers(...replacers)({ + isHtml, + mxEvent, + room, + shouldShowPillAvatar, + keywordRegexpPattern, + }); +}; + +/** + * ViewModel for EventContentBody component. + * Handles all Matrix SDK interactions and content processing. + */ +export class EventContentBodyViewModel + extends BaseViewModel + implements EventContentBodyViewModelInterface +{ + private static readonly computeBodySnapshot = ( + props: EventContentBodyViewModelProps, + ): Pick => { + const { content, stripReply, highlights, linkify, enableBigEmoji, mediaIsVisible } = props; + const isEmote = content.msgtype === MsgType.Emote; + const { strippedBody, formattedBody, emojiBodyElements, className } = bodyToNode(content, highlights, { + disableBigEmoji: isEmote || !enableBigEmoji, + stripReplyFallback: stripReply, + mediaIsVisible, + linkify, + }); + + return { + body: emojiBodyElements || strippedBody, + formattedBody, + className, + }; + }; + + private static readonly computeDir = (props: EventContentBodyViewModelProps): "auto" | undefined => { + const { as = "span", includeDir = true } = props; + return as === "div" || includeDir ? "auto" : undefined; + }; + + private static readonly parseFormattedBody = ( + formattedBody: string, + replacer?: Replacer, + ): ReturnType => parse(formattedBody, replacer ? { replace: replacer } : undefined); + + private static readonly computeSnapshot = (props: EventContentBodyViewModelProps): EventContentBodyViewSnapshot => { + const { body, formattedBody, className } = EventContentBodyViewModel.computeBodySnapshot(props); + const replacer = createReplacer(props); + const dir = EventContentBodyViewModel.computeDir(props); + + return { + body, + formattedBody, + replacer, + parseFormattedBody: EventContentBodyViewModel.parseFormattedBody, + className, + dir, + }; + }; + + public constructor(props: EventContentBodyViewModelProps) { + const propsWithSettingDefaults: EventContentBodyViewModelProps = { + ...props, + renderMentionPills: props.renderMentionPills ?? false, + renderKeywordPills: props.renderKeywordPills ?? false, + renderSpoilers: props.renderSpoilers ?? false, + renderCodeBlocks: props.renderCodeBlocks ?? false, + renderTooltipsForAmbiguousLinks: props.renderTooltipsForAmbiguousLinks ?? false, + enableBigEmoji: props.enableBigEmoji ?? SettingsStore.getValue("TextualBody.enableBigEmoji"), + shouldShowPillAvatar: props.shouldShowPillAvatar ?? SettingsStore.getValue("Pill.shouldShowPillAvatar"), + }; + + super(propsWithSettingDefaults, EventContentBodyViewModel.computeSnapshot(propsWithSettingDefaults)); + + const enableBigEmojiWatcherRef = SettingsStore.watchSetting( + "TextualBody.enableBigEmoji", + null, + (_settingName, _roomId, _level, _newValAtLevel, newVal) => { + this.setEnableBigEmoji(newVal); + }, + ); + this.disposables.track(() => SettingsStore.unwatchSetting(enableBigEmojiWatcherRef)); + + const shouldShowPillAvatarWatcherRef = SettingsStore.watchSetting( + "Pill.shouldShowPillAvatar", + null, + (_settingName, _roomId, _level, _newValAtLevel, newVal) => { + this.setShouldShowPillAvatar(newVal); + }, + ); + this.disposables.track(() => SettingsStore.unwatchSetting(shouldShowPillAvatarWatcherRef)); + } + + public setEventContent = (mxEvent: MatrixEvent | undefined, content: IContent): void => { + this.props.mxEvent = mxEvent; + this.props.content = content; + const { body, formattedBody, className } = EventContentBodyViewModel.computeBodySnapshot(this.props); + const replacer = createReplacer(this.props); + + this.snapshot.merge({ body, formattedBody, replacer, className }); + }; + + public setStripReply = (stripReply?: boolean): void => { + this.props.stripReply = stripReply; + const { body, formattedBody, className } = EventContentBodyViewModel.computeBodySnapshot(this.props); + + this.snapshot.merge({ body, formattedBody, className }); + }; + + public setHighlights = (highlights?: string[]): void => { + this.props.highlights = highlights; + const { body, formattedBody, className } = EventContentBodyViewModel.computeBodySnapshot(this.props); + + this.snapshot.merge({ body, formattedBody, className }); + }; + + public setAs = (as: "span" | "div"): void => { + this.props.as = as; + const dir = EventContentBodyViewModel.computeDir(this.props); + + this.snapshot.merge({ dir }); + }; + + public setEnableBigEmoji = (enableBigEmoji?: boolean): void => { + this.props.enableBigEmoji = enableBigEmoji; + const { body, formattedBody, className } = EventContentBodyViewModel.computeBodySnapshot(this.props); + + this.snapshot.merge({ body, formattedBody, className }); + }; + + public setShouldShowPillAvatar = (shouldShowPillAvatar?: boolean): void => { + this.props.shouldShowPillAvatar = shouldShowPillAvatar; + const replacer = createReplacer(this.props); + + this.snapshot.merge({ replacer }); + }; +} diff --git a/apps/web/test/unit-tests/components/views/messages/TextualBody-test.tsx b/apps/web/test/unit-tests/components/views/messages/TextualBody-test.tsx index 3c9daa5d44..924473a692 100644 --- a/apps/web/test/unit-tests/components/views/messages/TextualBody-test.tsx +++ b/apps/web/test/unit-tests/components/views/messages/TextualBody-test.tsx @@ -19,13 +19,14 @@ import { mkStubRoom, mockClientPushProcessor, } from "../../../../test-utils"; -import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import * as languageHandler from "../../../../../src/languageHandler"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import TextualBody from "../../../../../src/components/views/messages/TextualBody"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../../../src/contexts/RoomContext"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; import { type MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; +import { getRoomContext } from "../../../../test-utils/room"; const room1Id = "!room1:example.com"; const room2Id = "!room2:example.com"; @@ -57,7 +58,6 @@ const mkFormattedMessage = (body: string, formattedBody: string): MatrixEvent => describe("", () => { afterEach(() => { - jest.spyOn(MatrixClientPeg, "get").mockRestore(); jest.spyOn(global.Math, "random").mockRestore(); }); @@ -114,12 +114,17 @@ describe("", () => { jest.spyOn(global.Math, "random").mockReturnValue(0.123456); }); - const getComponent = (props = {}, matrixClient: MatrixClient = defaultMatrixClient, renderingFn?: any) => - (renderingFn ?? render)( + const getComponent = (props = {}, matrixClient: MatrixClient = defaultMatrixClient, renderingFn?: any) => { + const mergedProps = { ...defaultProps, ...props }; + const room = matrixClient.getRoom(mergedProps.mxEvent.getRoomId()) ?? defaultRoom; + return (renderingFn ?? render)( - + + + , ); + }; it("renders m.emote correctly", () => { DMRoomMap.makeShared(defaultMatrixClient); diff --git a/apps/web/test/viewmodels/message-body/EventContentBodyViewModel-test.tsx b/apps/web/test/viewmodels/message-body/EventContentBodyViewModel-test.tsx new file mode 100644 index 0000000000..1fbac26f84 --- /dev/null +++ b/apps/web/test/viewmodels/message-body/EventContentBodyViewModel-test.tsx @@ -0,0 +1,373 @@ +/* + * 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 { MsgType, PushRuleKind, type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix"; +import { type JSX } from "react"; + +import { + EventContentBodyViewModel, + type EventContentBodyViewModelProps, +} from "../../../src/viewmodels/message-body/EventContentBodyViewModel"; +import { stubClient, mkStubRoom, mkEvent } from "../../test-utils"; +import { bodyToNode } from "../../../src/HtmlUtils"; +import { + combineRenderers, + mentionPillRenderer, + keywordPillRenderer, + ambiguousLinkTooltipRenderer, + spoilerRenderer, + codeBlockRenderer, +} from "../../../src/renderer"; +import PlatformPeg from "../../../src/PlatformPeg"; +import type BasePlatform from "../../../src/BasePlatform"; +import SettingsStore from "../../../src/settings/SettingsStore"; + +jest.mock("../../../src/HtmlUtils", () => ({ + ...jest.requireActual("../../../src/HtmlUtils"), + bodyToNode: jest.fn(), +})); + +jest.mock("../../../src/renderer", () => ({ + combineRenderers: jest.fn(), + mentionPillRenderer: jest.fn(), + keywordPillRenderer: jest.fn(), + ambiguousLinkTooltipRenderer: jest.fn(), + codeBlockRenderer: jest.fn(), + spoilerRenderer: jest.fn(), +})); + +jest.mock("../../../src/PlatformPeg", () => ({ + __esModule: true, + default: { + get: jest.fn(), + }, +})); + +const mockedBodyToNode = jest.mocked(bodyToNode); +const mockedCombineRenderers = jest.mocked(combineRenderers); +const mockedPlatformPeg = jest.mocked(PlatformPeg); + +describe("EventContentBodyViewModel", () => { + const defaultContent = { + body: "Hello world", + msgtype: MsgType.Text, + }; + + const defaultProps = (overrides: Partial = {}): EventContentBodyViewModelProps => ({ + client: null, + content: defaultContent, + linkify: false, + as: "span", + ...overrides, + }); + + beforeEach(() => { + mockedBodyToNode.mockReset(); + mockedCombineRenderers.mockReset(); + mockedPlatformPeg.get.mockReset(); + mockedPlatformPeg.get.mockReturnValue(null); + }); + + it("passes render options to bodyToNode", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Hello world", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + + const vm = new EventContentBodyViewModel( + defaultProps({ + linkify: true, + stripReply: true, + enableBigEmoji: true, + mediaIsVisible: false, + }), + ); + const snapshot = vm.getSnapshot(); + + expect(mockedBodyToNode).toHaveBeenCalledWith(defaultContent, undefined, { + disableBigEmoji: false, + stripReplyFallback: true, + mediaIsVisible: false, + linkify: true, + }); + expect(snapshot.body).toBe("Hello world"); + expect(snapshot.replacer).toBe(replacer); + expect(snapshot.className).toContain("mx_EventTile_body"); + }); + + it("initializes setting-backed options from SettingsStore when omitted", () => { + const replacer = jest.fn(); + const createReplacerFromOptions = jest.fn().mockReturnValue(replacer); + mockedCombineRenderers.mockReturnValue(createReplacerFromOptions); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Hello world", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + const getValueSpy = jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => { + if (settingName === "TextualBody.enableBigEmoji") return false; + if (settingName === "Pill.shouldShowPillAvatar") return false; + return true; + }); + + new EventContentBodyViewModel(defaultProps()); + + expect(getValueSpy).toHaveBeenCalledWith("TextualBody.enableBigEmoji"); + expect(getValueSpy).toHaveBeenCalledWith("Pill.shouldShowPillAvatar"); + expect(mockedBodyToNode).toHaveBeenCalledWith( + defaultContent, + undefined, + expect.objectContaining({ disableBigEmoji: true }), + ); + expect(mockedCombineRenderers).toHaveBeenCalledWith(); + expect(createReplacerFromOptions).toHaveBeenCalledWith( + expect.objectContaining({ shouldShowPillAvatar: false }), + ); + getValueSpy.mockRestore(); + }); + + it("uses the injected client to resolve the room for renderer context", () => { + const replacer = jest.fn(); + const createReplacerFromOptions = jest.fn().mockReturnValue(replacer); + mockedCombineRenderers.mockReturnValue(createReplacerFromOptions); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Hello world", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + const client = stubClient(); + const mxEvent = mkEvent({ + type: "m.room.message", + room: "!room:example.org", + user: "@user:example.org", + content: defaultContent, + event: true, + }); + const room = mkStubRoom("!room:example.org", "Room", client) as Room; + const getRoomSpy = jest.spyOn(client, "getRoom").mockReturnValue(room); + + new EventContentBodyViewModel(defaultProps({ mxEvent, client })); + + expect(getRoomSpy).toHaveBeenCalledWith("!room:example.org"); + expect(createReplacerFromOptions).toHaveBeenCalledWith(expect.objectContaining({ room })); + }); + + it("forces disableBigEmoji for emote events", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Emote", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + + new EventContentBodyViewModel( + defaultProps({ + content: { + body: "Emote", + msgtype: MsgType.Emote, + }, + enableBigEmoji: true, + }), + ); + + expect(mockedBodyToNode).toHaveBeenCalledWith( + { body: "Emote", msgtype: MsgType.Emote }, + undefined, + expect.objectContaining({ disableBigEmoji: true }), + ); + }); + + it("uses parse when formattedBody is provided", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Hello world", + formattedBody: "Hello", + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + + const vm = new EventContentBodyViewModel(defaultProps()); + + const snapshot = vm.getSnapshot(); + + expect(snapshot.formattedBody).toBe("Hello"); + expect(snapshot.body).toBe("Hello world"); + expect(snapshot.replacer).toBe(replacer); + }); + + it("uses emojiBodyElements when provided", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + const emojiElements = ["emoji"] as unknown as JSX.Element[]; + mockedBodyToNode.mockReturnValue({ + strippedBody: "ignored", + formattedBody: undefined, + emojiBodyElements: emojiElements, + className: "mx_EventTile_body", + }); + + const vm = new EventContentBodyViewModel(defaultProps()); + + expect(vm.getSnapshot().body).toBe(emojiElements); + expect(vm.getSnapshot().replacer).toBe(replacer); + }); + + it("sets dir to auto for div elements even when includeDir is false", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Hello world", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + + const vm = new EventContentBodyViewModel(defaultProps({ as: "div", includeDir: false })); + + expect(vm.getSnapshot().dir).toBe("auto"); + }); + + it("omits dir when includeDir is false on span elements", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Hello world", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + + const vm = new EventContentBodyViewModel(defaultProps({ as: "span", includeDir: false })); + + expect(vm.getSnapshot().dir).toBeUndefined(); + }); + + it("updates snapshot when setEventContent changes content", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Initial", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + + const vm = new EventContentBodyViewModel(defaultProps()); + expect(vm.getSnapshot().body).toBe("Initial"); + + mockedBodyToNode.mockReturnValue({ + strippedBody: "Updated", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + + vm.setEventContent(undefined, { body: "Updated", msgtype: MsgType.Text }); + + expect(vm.getSnapshot().body).toBe("Updated"); + }); + + it("emits updates when setters are called with unchanged values", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Initial", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + + const vm = new EventContentBodyViewModel(defaultProps()); + const previousSnapshot = vm.getSnapshot(); + const subscriber = jest.fn(); + + vm.subscribe(subscriber); + vm.setEventContent(undefined, defaultContent); + vm.setAs("span"); + + expect(subscriber).toHaveBeenCalledTimes(2); + expect(vm.getSnapshot()).toEqual(previousSnapshot); + }); + + it("includes renderers based on options and platform capabilities", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Hello world", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + mockedPlatformPeg.get.mockReturnValue({ needsUrlTooltips: () => true } as unknown as BasePlatform); + + const client = stubClient(); + const mxEvent = mkEvent({ + type: "m.room.message", + room: "!room:example.org", + user: "@user:example.org", + content: defaultContent, + event: true, + }); + jest.spyOn(mxEvent, "getPushDetails").mockReturnValue({ + rule: { + enabled: true, + kind: PushRuleKind.ContentSpecific, + pattern: "Hello", + }, + } as unknown as ReturnType); + jest.spyOn(client, "getRoom").mockReturnValue(mkStubRoom("!room:example.org", "Room", client) as Room); + + new EventContentBodyViewModel( + defaultProps({ + renderMentionPills: true, + renderKeywordPills: true, + renderTooltipsForAmbiguousLinks: true, + renderSpoilers: true, + renderCodeBlocks: true, + mxEvent, + }), + ); + + expect(mockedCombineRenderers).toHaveBeenCalledWith( + mentionPillRenderer, + keywordPillRenderer, + ambiguousLinkTooltipRenderer, + spoilerRenderer, + codeBlockRenderer, + ); + }); + + it("skips tooltip renderer when platform does not need URL tooltips", () => { + const replacer = jest.fn(); + mockedCombineRenderers.mockReturnValue(() => replacer); + mockedBodyToNode.mockReturnValue({ + strippedBody: "Hello world", + formattedBody: undefined, + emojiBodyElements: undefined, + className: "mx_EventTile_body", + }); + mockedPlatformPeg.get.mockReturnValue({ needsUrlTooltips: () => false } as unknown as BasePlatform); + + new EventContentBodyViewModel( + defaultProps({ + renderMentionPills: true, + renderTooltipsForAmbiguousLinks: true, + }), + ); + + expect(mockedCombineRenderers).toHaveBeenCalledWith(mentionPillRenderer); + }); +}); diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/as-span-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/as-span-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..0c1ebb821882380c5a3b28fb05ee9ab4eba421e0 GIT binary patch literal 20247 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 Gr~V(mY+r-` literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/plain-text-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/plain-text-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..92698906e97deeda23e36e04c16b185a01b7abff GIT binary patch literal 20467 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 literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/with-highlight-auto.png b/packages/shared-components/__vis__/linux/__baselines__/message-body/EventContentBody/EventContentBody.stories.tsx/with-highlight-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..b3980d52246b85b793d9e85af779f5af90677062 GIT binary patch literal 21503 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 * { + display: inline; + } + } + } +} + +.EventTile_searchHighlight { + background-color: var(--cpd-color-text-action-accent); + color: var(--cpd-color-text-on-solid-primary); + border-radius: 5px; /* no compund for 5px */ + padding-inline: var(--cpd-space-0-5x); + cursor: pointer; + + a { + background-color: var(--cpd-color-text-action-accent); + color: var(--cpd-color-text-on-solid-primary); + } +} diff --git a/packages/shared-components/src/message-body/EventContentBody/EventContentBody.stories.tsx b/packages/shared-components/src/message-body/EventContentBody/EventContentBody.stories.tsx new file mode 100644 index 0000000000..dacac25f57 --- /dev/null +++ b/packages/shared-components/src/message-body/EventContentBody/EventContentBody.stories.tsx @@ -0,0 +1,92 @@ +/* +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 "../../viewmodel/useMockedViewModel"; +import { EventContentBodyView, type EventContentBodyViewSnapshot } from "./EventContentBodyView"; +import styles from "./EventContentBody.module.css"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; + +type EventContentBodyStoryProps = EventContentBodyViewSnapshot & { + as: "div" | "span"; +}; + +const EventContentBodyWrapperImpl = ({ as, ...snapshot }: EventContentBodyStoryProps): JSX.Element => { + const vm = useMockedViewModel(snapshot, {}); + return ; +}; +const EventContentBodyWrapper = withViewDocs(EventContentBodyWrapperImpl, EventContentBodyView); + +const meta = { + title: "MessageBody/EventContentBody", + component: EventContentBodyWrapper, + tags: ["autodocs"], + args: { + as: "div", + className: styles.EventTile_body, + dir: "auto", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const PlainText: Story = { + args: { + body: "Hello, this is a plain text message.", + className: styles.EventTile_body, + }, +}; + +export const BigEmoji: Story = { + args: { + body: [ + + 👋 + , + + 😊 + , + ], + className: `${styles.EventTile_body} ${styles.EventTile_bigEmoji}`, + }, +}; + +export const HtmlContent: Story = { + args: { + body: "This is bold and italic text with a link.", + formattedBody: + "

This is bold and italic text with a link.

", + className: `${styles.EventTile_body} ${styles.markdownBody}`, + }, +}; + +export const CodeBlock: Story = { + args: { + body: 'function hello() {\n console.log("Hello, world!");\n}', + formattedBody: '
function hello() {\n  console.log("Hello, world!");\n}
', + className: `${styles.EventTile_body} ${styles.markdownBody}`, + }, +}; + +export const AsSpan: Story = { + args: { + as: "span", + body: "This is rendered as a span element.", + className: styles.EventTile_body, + }, +}; + +export const WithHighlight: Story = { + args: { + body: "Message with a highlighted word.", + formattedBody: `Message with a highlighted word.`, + className: styles.EventTile_body, + }, +}; diff --git a/packages/shared-components/src/message-body/EventContentBody/EventContentBody.test.tsx b/packages/shared-components/src/message-body/EventContentBody/EventContentBody.test.tsx new file mode 100644 index 0000000000..d6a0559218 --- /dev/null +++ b/packages/shared-components/src/message-body/EventContentBody/EventContentBody.test.tsx @@ -0,0 +1,53 @@ +/* + 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, it, expect } from "vitest"; + +import * as stories from "./EventContentBody.stories"; + +const { PlainText, BigEmoji, HtmlContent, CodeBlock, AsSpan, WithHighlight } = composeStories(stories); + +describe("EventContentBodyView", () => { + it("renders plain text correctly", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders big emoji correctly", () => { + const { container } = render(<BigEmoji />); + expect(container).toMatchSnapshot(); + }); + + it("renders HTML content correctly", () => { + const { container } = render(<HtmlContent />); + expect(container).toMatchSnapshot(); + }); + + it("renders code block correctly", () => { + const { container } = render(<CodeBlock />); + expect(container).toMatchSnapshot(); + }); + + it("renders as span when specified", () => { + const { container } = render(<AsSpan />); + expect(container).toMatchSnapshot(); + expect(container.querySelector("span")).toBeInTheDocument(); + }); + + it("renders highlighted content correctly", () => { + const { container } = render(<WithHighlight />); + expect(container).toMatchSnapshot(); + }); + + it("displays expected text content", () => { + render(<PlainText />); + expect(screen.getByText("Hello, this is a plain text message.")).toBeInTheDocument(); + }); +}); diff --git a/packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx b/packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx new file mode 100644 index 0000000000..1202882dc8 --- /dev/null +++ b/packages/shared-components/src/message-body/EventContentBody/EventContentBodyView.tsx @@ -0,0 +1,110 @@ +/* +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, memo, type Ref } from "react"; +import parse, { type HTMLReactParserOptions } from "html-react-parser"; + +import { type ViewModel, useViewModel } from "../../viewmodel"; +import { applyReplacerOnString } from "../../utils/applyReplacerOnString"; + +type Replacer = HTMLReactParserOptions["replace"]; +type ParseFormattedBody = (formattedBody: string, replacer?: Replacer) => ReturnType<typeof parse>; + +/** + * Snapshot interface for the EventContentBody view. + */ +export interface EventContentBodyViewSnapshot { + /** + * The plain/emoji body content to render when no formatted body is available. + */ + body: string | JSX.Element[]; + /** + * The raw formatted body HTML, if available. + */ + formattedBody?: string; + /** + * The text/element replacer used for pills, spoilers, code blocks, etc. + */ + replacer?: Replacer; + /** + * Optional parser implementation for formatted bodies. + * This allows callers to provide a parser that matches their replacer implementation. + */ + parseFormattedBody?: ParseFormattedBody; + /** + * CSS class names to apply to the container element. + */ + className: string; + /** + * The text direction attribute. + * Always "auto" for divs, controlled by includeDir prop for spans. + */ + dir?: "auto"; +} + +export type EventContentBodyViewModel = ViewModel<EventContentBodyViewSnapshot>; + +interface EventContentBodyBaseViewProps { + /** + * The ViewModel providing the snapshot data. + */ + vm: EventContentBodyViewModel; +} + +export type EventContentBodyViewProps = EventContentBodyBaseViewProps & + ( + | { + /** + * Render the content in a span element. + */ + as: "span"; + /** + * Optional ref to forward to the rendered span element. + */ + ref?: Ref<HTMLSpanElement>; + } + | { + /** + * Render the content in a div element. + */ + as: "div"; + /** + * Optional ref to forward to the rendered div element. + */ + ref?: Ref<HTMLDivElement>; + } + ); + +/** + * View component for rendering Matrix event content body. + */ +export const EventContentBodyView = memo(function EventContentBodyView({ + vm, + as, + ref, +}: Readonly<EventContentBodyViewProps>): JSX.Element { + const { body, formattedBody, replacer, className, dir, parseFormattedBody } = useViewModel(vm); + const parseBody = + parseFormattedBody ?? + ((formatted: string, inputReplacer?: Replacer) => + parse(formatted, inputReplacer ? { replace: inputReplacer } : undefined)); + const children = formattedBody ? parseBody(formattedBody, replacer) : applyReplacerOnString(body, replacer); + + if (as === "span") { + return ( + <span ref={ref} className={className} dir={dir}> + {children} + </span> + ); + } + + return ( + <div ref={ref} className={className} dir={dir}> + {children} + </div> + ); +}); diff --git a/packages/shared-components/src/message-body/EventContentBody/__snapshots__/EventContentBody.test.tsx.snap b/packages/shared-components/src/message-body/EventContentBody/__snapshots__/EventContentBody.test.tsx.snap new file mode 100644 index 0000000000..614c94839e --- /dev/null +++ b/packages/shared-components/src/message-body/EventContentBody/__snapshots__/EventContentBody.test.tsx.snap @@ -0,0 +1,106 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`EventContentBodyView > renders HTML content correctly 1`] = ` +<div> + <div + class="EventTile_body markdownBody" + dir="auto" + > + <p> + This is + <strong> + bold + </strong> + and + <em> + italic + </em> + text with a + <a + href="https://matrix.org" + > + link + </a> + . + </p> + </div> +</div> +`; + +exports[`EventContentBodyView > renders as span when specified 1`] = ` +<div> + <span + class="EventTile_body" + dir="auto" + > + This is rendered as a span element. + </span> +</div> +`; + +exports[`EventContentBodyView > renders big emoji correctly 1`] = ` +<div> + <div + class="EventTile_body EventTile_bigEmoji" + dir="auto" + > + <span + class="Emoji" + title=":wave:" + > + 👋 + </span> + <span + class="Emoji" + title=":smile:" + > + 😊 + </span> + </div> +</div> +`; + +exports[`EventContentBodyView > renders code block correctly 1`] = ` +<div> + <div + class="EventTile_body markdownBody" + dir="auto" + > + <pre> + <code> + function hello() { + console.log("Hello, world!"); +} + </code> + </pre> + </div> +</div> +`; + +exports[`EventContentBodyView > renders highlighted content correctly 1`] = ` +<div> + <div + class="EventTile_body" + dir="auto" + > + Message with a + <span + class="EventTile_searchHighlight" + > + highlighted + </span> + word. + </div> +</div> +`; + +exports[`EventContentBodyView > renders plain text correctly 1`] = ` +<div> + <div + class="EventTile_body" + dir="auto" + > + Hello, this is a plain text message. + </div> +</div> +`; diff --git a/packages/shared-components/src/message-body/EventContentBody/index.tsx b/packages/shared-components/src/message-body/EventContentBody/index.tsx new file mode 100644 index 0000000000..71f6798037 --- /dev/null +++ b/packages/shared-components/src/message-body/EventContentBody/index.tsx @@ -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 { + EventContentBodyView, + type EventContentBodyViewSnapshot, + type EventContentBodyViewModel, + type EventContentBodyViewProps, +} from "./EventContentBodyView"; diff --git a/packages/shared-components/src/utils/applyReplacerOnString.tsx b/packages/shared-components/src/utils/applyReplacerOnString.tsx new file mode 100644 index 0000000000..334ae1fd99 --- /dev/null +++ b/packages/shared-components/src/utils/applyReplacerOnString.tsx @@ -0,0 +1,35 @@ +/* +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 { Text, type HTMLReactParserOptions } from "html-react-parser"; + +type Replacer = HTMLReactParserOptions["replace"]; + +/** + * Applies a parser replacer to string content while passing through JSX elements unchanged. + * + * @param input Plain-text body content or pre-rendered JSX elements (for example emoji bodies). + * Non-string items are returned verbatim. + * @param replacer Optional replace callback to run on string items. + * @returns The original `input` when no replacer is provided; otherwise an array where string + * items are replaced and JSX elements are passed through unchanged. + */ +export function applyReplacerOnString( + input: string | JSX.Element[], + replacer?: Replacer, +): JSX.Element | JSX.Element[] | string { + if (!replacer) return input; + + const arr = Array.isArray(input) ? input : [input]; + return arr.map((item, index): JSX.Element => { + if (typeof item === "string") { + return <React.Fragment key={index}>{(replacer(new Text(item), 0) as JSX.Element) || item}</React.Fragment>; + } + return item; + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35dee08fb3..33d8ce5686 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -775,6 +775,9 @@ importers: counterpart: specifier: ^0.18.6 version: 0.18.6 + html-react-parser: + specifier: ^5.2.2 + version: 5.2.17(@types/react@19.2.10)(react@19.2.4) lodash: specifier: npm:lodash-es@^4.17.21 version: lodash-es@4.17.23