From fbc1881fe926e2d01b891200419e385b13974c15 Mon Sep 17 00:00:00 2001 From: withoutwyatt Date: Tue, 24 Mar 2026 14:11:32 +0100 Subject: [PATCH] render formatted body in reply and editor --- src/app/components/message/Reply.tsx | 51 ++++++++++++++++++++++++++-- src/app/features/room/RoomInput.tsx | 44 ++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 57bf2af99a..b45970f22a 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -2,16 +2,30 @@ import { Box, Icon, Icons, Text, as, color, toRem } from 'folds'; import { EventTimelineSet, Room } from 'matrix-js-sdk'; import React, { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react'; import classNames from 'classnames'; +import parse, { HTMLReactParserOptions } from 'html-react-parser'; +import { Opts as LinkifyOpts } from 'linkifyjs'; import { getMemberDisplayName, trimReplyFromBody } from '../../utils/room'; import { getMxIdLocalPart } from '../../utils/matrix'; import { LinePlaceholder } from './placeholder'; import { randomNumberBetween } from '../../utils/common'; import * as css from './Reply.css'; import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content'; -import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser'; +import { + factoryRenderLinkifyWithMention, + getReactCustomHtmlParser, + LINKIFY_OPTS, + makeMentionCustomProps, + renderMatrixMention, + scaleSystemEmoji, +} from '../../plugins/react-custom-html-parser'; import { useRoomEvent } from '../../hooks/useRoomEvent'; import colorMXID from '../../../util/colorMXID'; import { GetMemberPowerTag } from '../../hooks/useMemberPowerTag'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useMentionClickHandler } from '../../hooks/useMentionClickHandler'; +import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { sanitizeCustomHtml } from '../../utils/sanitize'; type ReplyLayoutProps = { userColor?: string; @@ -83,8 +97,38 @@ export const Reply = as<'div', ReplyProps>( [timelineSet, replyEventId] ); const replyEvent = useRoomEvent(room, replyEventId, getFromLocalTimeline); + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const mentionClickHandler = useMentionClickHandler(room.roomId); + const spoilerClickHandler = useSpoilerClickHandler(); + const linkifyOpts = useMemo( + () => ({ + ...LINKIFY_OPTS, + render: factoryRenderLinkifyWithMention((href) => + renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler)) + ), + }), + [mx, room, mentionClickHandler] + ); + const htmlReactParserOptions = useMemo( + () => + getReactCustomHtmlParser(mx, room.roomId, { + linkifyOpts, + useAuthentication, + handleSpoilerClick: spoilerClickHandler, + handleMentionClick: mentionClickHandler, + }), + [mx, room, linkifyOpts, useAuthentication, spoilerClickHandler, mentionClickHandler] + ); - const { body } = replyEvent?.getContent() ?? {}; + const { body, format, formatted_body} = replyEvent?.getContent() ?? {}; + const customBody = + format === 'org.matrix.custom.html' + ? parse( + sanitizeCustomHtml(trimReplyFromBody(formatted_body.replace('
', ''))), + htmlReactParserOptions + ) + : null; const sender = replyEvent?.getSender(); const powerTag = sender ? getMemberPowerTag?.(sender) : undefined; const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined; @@ -99,6 +143,7 @@ export const Reply = as<'div', ReplyProps>( const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted'; const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; + const parsedBodyJSX = customBody ?? bodyJSX return ( @@ -120,7 +165,7 @@ export const Reply = as<'div', ReplyProps>( > {replyEvent !== undefined ? ( - {badEncryption ? : bodyJSX} + {badEncryption ? : parsedBodyJSX} ) : ( ( const isComposing = useComposingCheck(); + const mentionClickHandler = useMentionClickHandler(room.roomId); + const spoilerClickHandler = useSpoilerClickHandler(); + const linkifyOpts = useMemo( + () => ({ + ...LINKIFY_OPTS, + render: factoryRenderLinkifyWithMention((href) => + renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler)) + ), + }), + [mx, room, mentionClickHandler] + ); + const htmlReactParserOptions = useMemo( + () => + getReactCustomHtmlParser(mx, room.roomId, { + linkifyOpts, + useAuthentication, + handleSpoilerClick: spoilerClickHandler, + handleMentionClick: mentionClickHandler, + }), + [mx, room, linkifyOpts, useAuthentication, spoilerClickHandler, mentionClickHandler] + ); + useElementSizeObserver( useCallback(() => fileDropContainerRef.current, [fileDropContainerRef]), useCallback((width) => setHideStickerBtn(width < 500), []) @@ -574,7 +607,12 @@ export const RoomInput = forwardRef( } > - {trimReplyFromBody(replyDraft.body)} + {replyDraft.formattedBody + ? parse( + sanitizeCustomHtml(trimReplyFromBody(replyDraft.formattedBody.replace('
', ''))), + htmlReactParserOptions + ) + : trimReplyFromBody(replyDraft.body)}