From d0d209436d84eb81b989c02d3e7414d0e5dce36b Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Fri, 24 Apr 2026 19:27:19 -0300 Subject: [PATCH 01/17] add maxMessageParseSize parameter to parser --- .../message/hooks/useMaxMessageParseSize.ts | 18 +++++++++ .../message/hooks/useNormalizedMessage.ts | 15 ++++++-- .../message/variants/ThreadMessagePreview.tsx | 5 ++- .../variants/room/RoomMessageContent.tsx | 4 +- .../variants/thread/ThreadMessageContent.tsx | 4 +- apps/meteor/client/lib/constants.ts | 1 + .../lib/parseMessageTextToAstMarkdown.ts | 38 ++++++++++++++----- .../room/MessageList/hooks/useMessageBody.tsx | 7 ++-- 8 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts diff --git a/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts b/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts new file mode 100644 index 0000000000000..60455df63fa4f --- /dev/null +++ b/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts @@ -0,0 +1,18 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { MESSAGE_PARSE_HARD_LIMIT } from '../../../lib/constants'; + +/** + * Returns the maximum allowed size for message parsing. + * Uses Math.min to ensure it never exceeds the hard limit to avoid performance issues. + * Always returns a number, never null or undefined. + */ +export const useMaxMessageParseSize = (): number => { + const settingValue = useSetting('Message_MaxAllowedSize', 5000); + + return useMemo(() => { + const maxSize = typeof settingValue === 'number' ? settingValue : 5000; + return Math.min(maxSize, MESSAGE_PARSE_HARD_LIMIT); + }, [settingValue]); +}; diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index a2a8047560ee4..ffba5aafc0491 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -57,7 +57,7 @@ const normalizeAttachments = (attachments: MessageAttachment[], name?: string, t }); }; -export const useNormalizedMessage = (message: TMessage): MessageWithMdEnforced => { +export const useNormalizedMessage = (message: TMessage, maxMessageParseSize: number): MessageWithMdEnforced => { const katex = useMessageListKatex(); const katexEnabled = !!katex; const customDomains = useAutoLinkDomains(); @@ -77,7 +77,7 @@ export const useNormalizedMessage = (message: TMessag }), }; - const normalizedMessage = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + const normalizedMessage = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions, maxMessageParseSize); if (normalizedMessage.attachments) { normalizedMessage.attachments = normalizeAttachments( @@ -88,5 +88,14 @@ export const useNormalizedMessage = (message: TMessag } return normalizedMessage; - }, [showColors, customDomains, katexEnabled, katex?.dollarSyntaxEnabled, katex?.parenthesisSyntaxEnabled, message, autoTranslateOptions]); + }, [ + showColors, + customDomains, + katexEnabled, + katex?.dollarSyntaxEnabled, + katex?.parenthesisSyntaxEnabled, + message, + autoTranslateOptions, + maxMessageParseSize, + ]); }; diff --git a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx index 7aec9412fd07d..1723cd0675948 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx @@ -31,6 +31,7 @@ import { useGoToThread } from '../../../views/room/hooks/useGoToThread'; import Emoji from '../../Emoji'; import { useShowTranslated } from '../list/MessageListContext'; import ThreadMessagePreviewBody from './threadPreview/ThreadMessagePreviewBody'; +import { useMaxMessageParseSize } from '../hooks/useMaxMessageParseSize'; type ThreadMessagePreviewProps = { message: IThreadMessage; @@ -51,7 +52,9 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }: useCountSelected(); const messageType = parentMessage.isSuccess ? MessageTypes.getType(parentMessage.data) : null; - const messageBody = useMessageBody(parentMessage.data); + + const maxMessageParseSize = useMaxMessageParseSize(); + const messageBody = useMessageBody(parentMessage.data, maxMessageParseSize); const previewMessage = isParsedMessage(messageBody) ? { md: messageBody } : { msg: messageBody }; diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 813d63e9f9130..6a6a7a05af46f 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -18,6 +18,7 @@ import MessageActions from '../../content/MessageActions'; import Reactions from '../../content/Reactions'; import ThreadMetrics from '../../content/ThreadMetrics'; import UrlPreviews from '../../content/UrlPreviews'; +import { useMaxMessageParseSize } from '../../hooks/useMaxMessageParseSize'; import { useNormalizedMessage } from '../../hooks/useNormalizedMessage'; import { useOembedLayout } from '../../hooks/useOembedLayout'; import { useSubscriptionFromMessageQuery } from '../../hooks/useSubscriptionFromMessageQuery'; @@ -42,8 +43,9 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const chat = useChat(); const { t } = useTranslation(); + const maxMessageParseSize = useMaxMessageParseSize(); - const normalizedMessage = useNormalizedMessage(message); + const normalizedMessage = useNormalizedMessage(message, maxMessageParseSize); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; const quotes = normalizedMessage?.attachments?.filter(isQuoteAttachment) || []; diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index d137162d0b06d..1f26ec6cd6431 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -15,6 +15,7 @@ import Location from '../../content/Location'; import MessageActions from '../../content/MessageActions'; import Reactions from '../../content/Reactions'; import UrlPreviews from '../../content/UrlPreviews'; +import { useMaxMessageParseSize } from '../../hooks/useMaxMessageParseSize'; import { useNormalizedMessage } from '../../hooks/useNormalizedMessage'; import { useOembedLayout } from '../../hooks/useOembedLayout'; import { useSubscriptionFromMessageQuery } from '../../hooks/useSubscriptionFromMessageQuery'; @@ -33,10 +34,11 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const uid = useUserId(); const { enabled: readReceiptEnabled } = useMessageListReadReceipts(); const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; + const maxMessageParseSize = useMaxMessageParseSize(); const { t } = useTranslation(); - const normalizedMessage = useNormalizedMessage(message); + const normalizedMessage = useNormalizedMessage(message, maxMessageParseSize); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; diff --git a/apps/meteor/client/lib/constants.ts b/apps/meteor/client/lib/constants.ts index 70cae5deff224..158559d9e28eb 100644 --- a/apps/meteor/client/lib/constants.ts +++ b/apps/meteor/client/lib/constants.ts @@ -3,3 +3,4 @@ export const BIO_TEXT_MAX_LENGTH = 260; export const VIDEOCONF_STACK_MAX_USERS = 6; export const NAVIGATION_REGION_ID = 'navigation-region'; export const MAX_FILE_SIZE_PREVIEW = 10485760; // 10MB +export const MESSAGE_PARSE_HARD_LIMIT = 5000; diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts index 55d393cee38ed..b300309128687 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts @@ -28,6 +28,7 @@ export const parseMessageTextToAstMarkdown = < message: TMessage, parseOptions: Options, autoTranslateOptions: AutoTranslateOptions, + maxMessageParseSize: number, ): MessageWithMdEnforced => { const msg = removePossibleNullMessageValues(message); const { showAutoTranslate, autoTranslateLanguage } = autoTranslateOptions; @@ -35,12 +36,14 @@ export const parseMessageTextToAstMarkdown = < const translated = showAutoTranslate(message); const text = (translated && translations && translations[autoTranslateLanguage]) || msg.msg; - return { ...msg, - md: isE2EEMessage(message) || translated ? textToMessageToken(text, parseOptions) : (msg.md ?? textToMessageToken(text, parseOptions)), + md: + isE2EEMessage(message) || translated + ? textToMessageToken(text, parseOptions, maxMessageParseSize) + : (msg.md ?? textToMessageToken(text, parseOptions, maxMessageParseSize)), ...(msg.attachments && { - attachments: parseMessageAttachments(msg.attachments, parseOptions, { autoTranslateLanguage, translated }), + attachments: parseMessageAttachments(msg.attachments, parseOptions, { autoTranslateLanguage, translated }, maxMessageParseSize), }), }; }; @@ -49,6 +52,7 @@ export const parseMessageAttachment = ( attachment: T, parseOptions: Options, autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, + maxMessageParseSize: number, ): T => { const { translated, autoTranslateLanguage } = autoTranslateOptions; if (!attachment.text) { @@ -56,7 +60,7 @@ export const parseMessageAttachment = ( } if (isQuoteAttachment(attachment) && attachment.attachments) { - attachment.attachments = parseMessageAttachments(attachment.attachments, parseOptions, autoTranslateOptions); + attachment.attachments = parseMessageAttachments(attachment.attachments, parseOptions, autoTranslateOptions, maxMessageParseSize); } const text = @@ -66,7 +70,9 @@ export const parseMessageAttachment = ( return { ...attachment, - md: translated ? textToMessageToken(text, parseOptions) : (attachment.md ?? textToMessageToken(text, parseOptions)), + md: translated + ? textToMessageToken(text, parseOptions, maxMessageParseSize) + : (attachment.md ?? textToMessageToken(text, parseOptions, maxMessageParseSize)), }; }; @@ -74,7 +80,8 @@ export const parseMessageAttachments = ( attachments: T[], parseOptions: Options, autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, -): T[] => attachments.map((attachment) => parseMessageAttachment(attachment, parseOptions, autoTranslateOptions)); + maxMessageParseSize: number, +): T[] => attachments.map((attachment) => parseMessageAttachment(attachment, parseOptions, autoTranslateOptions, maxMessageParseSize)); const isNotNullOrUndefined = (value: unknown): boolean => value !== null && value !== undefined; @@ -105,7 +112,7 @@ export const removePossibleNullMessageValues = { +const textToMessageToken = (textOrRoot: string | Root, parseOptions: Options, maxMessageParseSize: number): Root => { if (!textOrRoot) { return []; } @@ -113,9 +120,22 @@ const textToMessageToken = (textOrRoot: string | Root, parseOptions: Options): R if (isParsedMessage(textOrRoot)) { return textOrRoot; } - const parsedMessage = parse(textOrRoot, parseOptions); + if (textOrRoot.length > maxMessageParseSize) { + return [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: textOrRoot, + }, + ], + }, + ]; + } + const result = parse(textOrRoot, parseOptions); - const parsedMessageCleaned = parsedMessage[0].type !== 'LINE_BREAK' ? parsedMessage : (parsedMessage.slice(1) as Root); + const parsedMessageCleaned = result[0].type !== 'LINE_BREAK' ? result : (result.slice(1) as Root); return parsedMessageCleaned; }; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx index 94313e6925432..1cf96b432bbac 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx @@ -6,7 +6,7 @@ import { useAutoLinkDomains } from './useAutoLinkDomains'; import { useMessageListAutoTranslate } from '../../../../components/message/list/MessageListContext'; import { parseMessageTextToAstMarkdown } from '../../../../lib/parseMessageTextToAstMarkdown'; -export const useMessageBody = (message: IMessage | undefined): string | Root => { +export const useMessageBody = (message: IMessage | undefined, maxMessageParseSize: number): string | Root => { const autoTranslateOptions = useMessageListAutoTranslate(); const customDomains = useAutoLinkDomains(); @@ -20,8 +20,7 @@ export const useMessageBody = (message: IMessage | undefined): string | Root => customDomains, emoticons: true, }; - - const messageWithMd = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); + const messageWithMd = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions, maxMessageParseSize); return messageWithMd.md; } @@ -39,5 +38,5 @@ export const useMessageBody = (message: IMessage | undefined): string | Root => } return ''; - }, [message, customDomains, autoTranslateOptions]); + }, [message, customDomains, autoTranslateOptions, maxMessageParseSize]); }; From c8bf7b29882ee2d6f351c6b98d044f3d741ce94e Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Sat, 25 Apr 2026 16:44:27 -0300 Subject: [PATCH 02/17] prevent parsing upstream instead of lower level code --- .../content/attachments/QuoteAttachment.tsx | 8 +++- .../message/hooks/useNormalizedMessage.ts | 15 +++++++- apps/meteor/client/lib/constants.ts | 2 +- .../lib/parseMessageTextToAstMarkdown.ts | 38 +++++-------------- .../room/MessageList/hooks/useMessageBody.tsx | 7 +++- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx index 2a4f975c6641a..386a2a7f37a97 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx @@ -13,6 +13,7 @@ import AttachmentAuthorName from './structure/AttachmentAuthorName'; import AttachmentContent from './structure/AttachmentContent'; import AttachmentDetails from './structure/AttachmentDetails'; import AttachmentInner from './structure/AttachmentInner'; +import { useMaxMessageParseSize } from '../../hooks/useMaxMessageParseSize'; // TODO: remove this team collaboration const quoteStyles = css` @@ -38,6 +39,7 @@ type QuoteAttachmentProps = { export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElement => { const formatTime = useTimeAgo(); const displayAvatarPreference = useUserPreference('displayAvatars'); + const maxMessageParseSize = useMaxMessageParseSize(); return ( <> @@ -71,7 +73,11 @@ export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElem )} - {attachment.md ? : attachment.text.substring(attachment.text.indexOf('\n') + 1)} + {attachment.text?.length <= maxMessageParseSize && attachment.md ? ( + + ) : ( + attachment.text.substring(attachment.text.indexOf('\n') + 1) + )} diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index ffba5aafc0491..86f8696c58635 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -77,7 +77,20 @@ export const useNormalizedMessage = (message: TMessag }), }; - const normalizedMessage = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions, maxMessageParseSize); + if (message.msg && message.msg.length > maxMessageParseSize) { + return { + ...message, + md: [ + { + type: 'PARAGRAPH', + value: [{ type: 'PLAIN_TEXT', value: message.msg }], + }, + ], + attachments: message.attachments, + }; + } + + const normalizedMessage = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); if (normalizedMessage.attachments) { normalizedMessage.attachments = normalizeAttachments( diff --git a/apps/meteor/client/lib/constants.ts b/apps/meteor/client/lib/constants.ts index 158559d9e28eb..4ac62d2e80cb3 100644 --- a/apps/meteor/client/lib/constants.ts +++ b/apps/meteor/client/lib/constants.ts @@ -3,4 +3,4 @@ export const BIO_TEXT_MAX_LENGTH = 260; export const VIDEOCONF_STACK_MAX_USERS = 6; export const NAVIGATION_REGION_ID = 'navigation-region'; export const MAX_FILE_SIZE_PREVIEW = 10485760; // 10MB -export const MESSAGE_PARSE_HARD_LIMIT = 5000; +export const MESSAGE_PARSE_HARD_LIMIT = 10000; diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts index b300309128687..55d393cee38ed 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts @@ -28,7 +28,6 @@ export const parseMessageTextToAstMarkdown = < message: TMessage, parseOptions: Options, autoTranslateOptions: AutoTranslateOptions, - maxMessageParseSize: number, ): MessageWithMdEnforced => { const msg = removePossibleNullMessageValues(message); const { showAutoTranslate, autoTranslateLanguage } = autoTranslateOptions; @@ -36,14 +35,12 @@ export const parseMessageTextToAstMarkdown = < const translated = showAutoTranslate(message); const text = (translated && translations && translations[autoTranslateLanguage]) || msg.msg; + return { ...msg, - md: - isE2EEMessage(message) || translated - ? textToMessageToken(text, parseOptions, maxMessageParseSize) - : (msg.md ?? textToMessageToken(text, parseOptions, maxMessageParseSize)), + md: isE2EEMessage(message) || translated ? textToMessageToken(text, parseOptions) : (msg.md ?? textToMessageToken(text, parseOptions)), ...(msg.attachments && { - attachments: parseMessageAttachments(msg.attachments, parseOptions, { autoTranslateLanguage, translated }, maxMessageParseSize), + attachments: parseMessageAttachments(msg.attachments, parseOptions, { autoTranslateLanguage, translated }), }), }; }; @@ -52,7 +49,6 @@ export const parseMessageAttachment = ( attachment: T, parseOptions: Options, autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, - maxMessageParseSize: number, ): T => { const { translated, autoTranslateLanguage } = autoTranslateOptions; if (!attachment.text) { @@ -60,7 +56,7 @@ export const parseMessageAttachment = ( } if (isQuoteAttachment(attachment) && attachment.attachments) { - attachment.attachments = parseMessageAttachments(attachment.attachments, parseOptions, autoTranslateOptions, maxMessageParseSize); + attachment.attachments = parseMessageAttachments(attachment.attachments, parseOptions, autoTranslateOptions); } const text = @@ -70,9 +66,7 @@ export const parseMessageAttachment = ( return { ...attachment, - md: translated - ? textToMessageToken(text, parseOptions, maxMessageParseSize) - : (attachment.md ?? textToMessageToken(text, parseOptions, maxMessageParseSize)), + md: translated ? textToMessageToken(text, parseOptions) : (attachment.md ?? textToMessageToken(text, parseOptions)), }; }; @@ -80,8 +74,7 @@ export const parseMessageAttachments = ( attachments: T[], parseOptions: Options, autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, - maxMessageParseSize: number, -): T[] => attachments.map((attachment) => parseMessageAttachment(attachment, parseOptions, autoTranslateOptions, maxMessageParseSize)); +): T[] => attachments.map((attachment) => parseMessageAttachment(attachment, parseOptions, autoTranslateOptions)); const isNotNullOrUndefined = (value: unknown): boolean => value !== null && value !== undefined; @@ -112,7 +105,7 @@ export const removePossibleNullMessageValues = { +const textToMessageToken = (textOrRoot: string | Root, parseOptions: Options): Root => { if (!textOrRoot) { return []; } @@ -120,22 +113,9 @@ const textToMessageToken = (textOrRoot: string | Root, parseOptions: Options, ma if (isParsedMessage(textOrRoot)) { return textOrRoot; } - if (textOrRoot.length > maxMessageParseSize) { - return [ - { - type: 'PARAGRAPH', - value: [ - { - type: 'PLAIN_TEXT', - value: textOrRoot, - }, - ], - }, - ]; - } - const result = parse(textOrRoot, parseOptions); + const parsedMessage = parse(textOrRoot, parseOptions); - const parsedMessageCleaned = result[0].type !== 'LINE_BREAK' ? result : (result.slice(1) as Root); + const parsedMessageCleaned = parsedMessage[0].type !== 'LINE_BREAK' ? parsedMessage : (parsedMessage.slice(1) as Root); return parsedMessageCleaned; }; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx index 1cf96b432bbac..09c1201a9cced 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx @@ -15,12 +15,17 @@ export const useMessageBody = (message: IMessage | undefined, maxMessageParseSiz return ''; } + if (message.msg && message.msg.length > maxMessageParseSize) { + return message.msg; + } + if (message.md) { const parseOptions: Options = { customDomains, emoticons: true, }; - const messageWithMd = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions, maxMessageParseSize); + + const messageWithMd = parseMessageTextToAstMarkdown(message, parseOptions, autoTranslateOptions); return messageWithMd.md; } From 1c1c10d3c4efa48dbe79a0343eeb094b97417026 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 27 Apr 2026 15:19:38 -0300 Subject: [PATCH 03/17] add positive number validation --- .../client/components/message/hooks/useMaxMessageParseSize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts b/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts index 60455df63fa4f..342a03aa0d7a5 100644 --- a/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts +++ b/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts @@ -12,7 +12,7 @@ export const useMaxMessageParseSize = (): number => { const settingValue = useSetting('Message_MaxAllowedSize', 5000); return useMemo(() => { - const maxSize = typeof settingValue === 'number' ? settingValue : 5000; + const maxSize = typeof settingValue === 'number' && settingValue > 0 ? settingValue : 5000; return Math.min(maxSize, MESSAGE_PARSE_HARD_LIMIT); }, [settingValue]); }; From 36d810cfdf736cb5ffd6ba52963a7f745525fe1d Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 27 Apr 2026 17:13:17 -0300 Subject: [PATCH 04/17] add unit tests --- .../attachments/QuoteAttachment.spec.tsx | 44 ++++++++++++ .../hooks/useNormalizedMessage.spec.ts | 68 +++++++++++++++++++ .../MessageList/hooks/useMessageBody.spec.ts | 64 +++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx create mode 100644 apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts create mode 100644 apps/meteor/client/views/room/MessageList/hooks/useMessageBody.spec.ts diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx new file mode 100644 index 0000000000000..315bbf103da46 --- /dev/null +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx @@ -0,0 +1,44 @@ +import { render, screen } from '@testing-library/react'; + +import { QuoteAttachment } from './QuoteAttachment'; + +jest.mock('../../hooks/useMaxMessageParseSize', () => ({ + useMaxMessageParseSize: () => 100, +})); + +jest.mock('@rocket.chat/ui-contexts', () => ({ + useUserPreference: () => true, +})); + +jest.mock('../../../../hooks/useTimeAgo', () => ({ + useTimeAgo: () => (date: Date) => date.toISOString(), +})); + +jest.mock('../../MessageContentBody', () => ({ + __esModule: true, + default: () =>
, +})); + +const baseAttachment = { + author_name: 'User', + author_icon: '', + ts: new Date(), + text: 'short text', + md: [{ type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'short text' }] }], +}; + +describe('QuoteAttachment', () => { + it('renders MessageContentBody when text length is within maxMessageParseSize', () => { + render(); + expect(screen.getByTestId('message-content-body')).toBeInTheDocument(); + }); + + it('renders plain text when text exceeds maxMessageParseSize', () => { + const longText = 'a'.repeat(101); + const attachment = { ...baseAttachment, text: longText }; + + render(); + + expect(screen.queryByTestId('message-content-body')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts new file mode 100644 index 0000000000000..09e4b4f82811e --- /dev/null +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts @@ -0,0 +1,68 @@ +import { renderHook } from '@testing-library/react'; + +import { useNormalizedMessage } from './useNormalizedMessage'; + +const mockParseMessageTextToAstMarkdown = jest.fn((msg: any, ..._args: any[]) => msg); + +jest.mock('../list/MessageListContext', () => ({ + useMessageListKatex: () => null, + useMessageListAutoTranslate: () => ({ + showAutoTranslate: () => false, + autoTranslateLanguage: '', + }), + useMessageListShowColors: () => false, +})); + +jest.mock('../../../lib/parseMessageTextToAstMarkdown', () => ({ + parseMessageTextToAstMarkdown: (msg: any, ...args: any[]) => mockParseMessageTextToAstMarkdown(msg, ...args), +})); + +jest.mock('../../../views/room/MessageList/hooks/useAutoLinkDomains', () => ({ useAutoLinkDomains: () => [] })); + +const baseMessage = { + _id: 'msg1', + rid: 'room1', + u: { _id: 'u1', username: 'user', name: 'User' }, + ts: new Date(), + _updatedAt: new Date(), +}; + +describe('useNormalizedMessage', () => { + beforeEach(() => { + mockParseMessageTextToAstMarkdown.mockClear(); + }); + + it('should skip parsing and returns PARAGRAPH node when msg exceeds maxMessageParseSize', () => { + const longMsg = 'a'.repeat(101); + const message = { ...baseMessage, msg: longMsg }; + + const { result } = renderHook(() => useNormalizedMessage(message as any, 100)); + + expect(mockParseMessageTextToAstMarkdown).not.toHaveBeenCalled(); + expect(result.current.md).toEqual([ + { + type: 'PARAGRAPH', + value: [{ type: 'PLAIN_TEXT', value: longMsg }], + }, + ]); + }); + + it('should call parseMessageTextToAstMarkdown when msg is within maxMessageParseSize', () => { + const message = { ...baseMessage, msg: 'Hello world' }; + + renderHook(() => useNormalizedMessage(message as any, 100)); + + expect(mockParseMessageTextToAstMarkdown).toHaveBeenCalledWith(message, expect.anything(), expect.anything()); + }); + + it('should preserve attachments when bypassing parsing due to size', () => { + const longMsg = 'a'.repeat(101); + const attachments = [{ type: 'quote', text: 'quoted' }]; + const message = { ...baseMessage, msg: longMsg, attachments }; + + const { result } = renderHook(() => useNormalizedMessage(message as any, 100)); + + expect(mockParseMessageTextToAstMarkdown).not.toHaveBeenCalled(); + expect(result.current.attachments).toBe(attachments); + }); +}); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.spec.ts b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.spec.ts new file mode 100644 index 0000000000000..6890a3a7f4f0e --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.spec.ts @@ -0,0 +1,64 @@ +import { renderHook } from '@testing-library/react'; + +import { useMessageBody } from './useMessageBody'; + +const mockParseMessageTextToAstMarkdown = jest.fn(); + +jest.mock('./useAutoLinkDomains', () => ({ + useAutoLinkDomains: () => [], +})); + +jest.mock('../../../../components/message/list/MessageListContext', () => ({ + useMessageListAutoTranslate: () => ({ + showAutoTranslate: () => false, + autoTranslateLanguage: '', + }), +})); + +jest.mock('../../../../lib/parseMessageTextToAstMarkdown', () => ({ + parseMessageTextToAstMarkdown: (msg: any, ...args: any[]) => mockParseMessageTextToAstMarkdown(msg, ...args), +})); + +const baseMessage = { + _id: 'msg1', + rid: 'room1', + u: { _id: 'u1', username: 'user', name: 'User' }, + ts: new Date(), + _updatedAt: new Date(), +}; + +describe('useMessageBody', () => { + beforeEach(() => { + mockParseMessageTextToAstMarkdown.mockClear(); + }); + + it('should return raw msg and skips parsing when msg exceeds maxMessageParseSize', () => { + const longMsg = 'a'.repeat(101); + const message = { ...baseMessage, msg: longMsg, md: [{ type: 'PARAGRAPH', value: [] }] }; + + const { result } = renderHook(() => useMessageBody(message as any, 100)); + + expect(result.current).toBe(longMsg); + expect(mockParseMessageTextToAstMarkdown).not.toHaveBeenCalled(); + }); + + it('should call parser when message has md and is within maxMessageParseSize', () => { + const md = [{ type: 'PARAGRAPH', value: [] }]; + const message = { ...baseMessage, msg: 'Hello world', md }; + mockParseMessageTextToAstMarkdown.mockReturnValue({ ...message, md }); + + const { result } = renderHook(() => useMessageBody(message as any, 100)); + + expect(mockParseMessageTextToAstMarkdown).toHaveBeenCalledWith(message, expect.anything(), expect.anything()); + expect(result.current).toBe(md); + }); + + it('should return raw msg without parsing when message has no md', () => { + const message = { ...baseMessage, msg: 'Hello world' }; + + const { result } = renderHook(() => useMessageBody(message as any, 100)); + + expect(mockParseMessageTextToAstMarkdown).not.toHaveBeenCalled(); + expect(result.current).toBe('Hello world'); + }); +}); From bd4a7c21a404b912443a213f10f0d5c3f2a300a7 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 27 Apr 2026 18:09:41 -0300 Subject: [PATCH 05/17] modify const --- apps/meteor/client/lib/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/lib/constants.ts b/apps/meteor/client/lib/constants.ts index 4ac62d2e80cb3..cd57c13d92078 100644 --- a/apps/meteor/client/lib/constants.ts +++ b/apps/meteor/client/lib/constants.ts @@ -3,4 +3,4 @@ export const BIO_TEXT_MAX_LENGTH = 260; export const VIDEOCONF_STACK_MAX_USERS = 6; export const NAVIGATION_REGION_ID = 'navigation-region'; export const MAX_FILE_SIZE_PREVIEW = 10485760; // 10MB -export const MESSAGE_PARSE_HARD_LIMIT = 10000; +export const MESSAGE_PARSE_HARD_LIMIT = 20000; From a5da1912377076a7299d4fcd52de3b5a49d2824d Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 27 Apr 2026 18:30:59 -0300 Subject: [PATCH 06/17] add normalizeAttachment to parse bypass scenario --- .../client/components/message/hooks/useNormalizedMessage.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index 86f8696c58635..8fda6978c0e46 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -86,7 +86,9 @@ export const useNormalizedMessage = (message: TMessag value: [{ type: 'PLAIN_TEXT', value: message.msg }], }, ], - attachments: message.attachments, + attachments: message.attachments + ? normalizeAttachments(message.attachments, message.file?.name, message.file?.type) + : message.attachments, }; } From b3bf08da92a24b3e8e5c2179c0a6be2725739c70 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 27 Apr 2026 18:42:29 -0300 Subject: [PATCH 07/17] remove unneeded optional chaining operator --- .../components/message/content/attachments/QuoteAttachment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx index 386a2a7f37a97..271de136a2e8a 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx @@ -73,7 +73,7 @@ export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElem )} - {attachment.text?.length <= maxMessageParseSize && attachment.md ? ( + {attachment.text.length <= maxMessageParseSize && attachment.md ? ( ) : ( attachment.text.substring(attachment.text.indexOf('\n') + 1) From cbd1db1cfe01b104f4998f49cd8d0aceb2bbe8b0 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 27 Apr 2026 18:56:42 -0300 Subject: [PATCH 08/17] add setting description --- apps/meteor/server/settings/message.ts | 1 + packages/i18n/src/locales/en.i18n.json | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/meteor/server/settings/message.ts b/apps/meteor/server/settings/message.ts index 734d3831eebef..f1d6f433616b2 100644 --- a/apps/meteor/server/settings/message.ts +++ b/apps/meteor/server/settings/message.ts @@ -168,6 +168,7 @@ export const createMessageSettings = () => await this.add('Message_MaxAllowedSize', 5000, { type: 'int', public: true, + i18nDescription: 'Message_MaxAllowedSize_Description', }); await this.add('Message_AllowConvertLongMessagesToAttachment', true, { type: 'boolean', diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 52113dc9a07b8..8403a6c57deae 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3517,6 +3517,7 @@ "Message_KeepHistory": "Keep Per Message Editing History", "Message_MaxAll": "Maximum Channel Size for ALL Message", "Message_MaxAllowedSize": "Maximum Allowed Characters Per Message", + "Message_MaxAllowedSize_Description": "Maximum number of characters allowed per message. Messages exceeding this limit will be displayed as plain text without markdown parsing.", "Message_not_sent_try_again": "Message not sent. \nPlease try again", "Message_QuoteChainLimit": "Maximum Number of Chained Quotes", "Message_Read_Receipt_Enabled": "Show Read Receipts", From 8dc377a0fe57515b876a75d482d3f260dd8a1542 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 27 Apr 2026 19:26:46 -0300 Subject: [PATCH 09/17] fix unit test --- .../components/message/hooks/useNormalizedMessage.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts index 09e4b4f82811e..b16ad0bd55699 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts @@ -63,6 +63,6 @@ describe('useNormalizedMessage', () => { const { result } = renderHook(() => useNormalizedMessage(message as any, 100)); expect(mockParseMessageTextToAstMarkdown).not.toHaveBeenCalled(); - expect(result.current.attachments).toBe(attachments); + expect(result.current.attachments).toEqual(attachments); }); }); From 43e69554489da963c58d5540c935e4fa5bdfce1a Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Mon, 27 Apr 2026 19:29:19 -0300 Subject: [PATCH 10/17] add changeset --- .changeset/cool-flowers-stop.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/cool-flowers-stop.md diff --git a/.changeset/cool-flowers-stop.md b/.changeset/cool-flowers-stop.md new file mode 100644 index 0000000000000..c45b3306ffc0c --- /dev/null +++ b/.changeset/cool-flowers-stop.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes workspace freezing when rendering massive markdown messages From 039b964f4159178768ee58b00603da1f9faaf482 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 28 Apr 2026 21:42:23 -0300 Subject: [PATCH 11/17] add text assertion in test --- .../message/content/attachments/QuoteAttachment.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx index 315bbf103da46..afbb090524ee0 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx @@ -40,5 +40,6 @@ describe('QuoteAttachment', () => { render(); expect(screen.queryByTestId('message-content-body')).not.toBeInTheDocument(); + expect(screen.getByText(longText)).toBeInTheDocument(); }); }); From 3b4ae966bfc2d8396deea06a139a999241b18760 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 28 Apr 2026 21:44:22 -0300 Subject: [PATCH 12/17] improve i18n description --- apps/meteor/server/settings/message.ts | 1 + packages/i18n/src/locales/en.i18n.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/server/settings/message.ts b/apps/meteor/server/settings/message.ts index f1d6f433616b2..57923586e658f 100644 --- a/apps/meteor/server/settings/message.ts +++ b/apps/meteor/server/settings/message.ts @@ -168,6 +168,7 @@ export const createMessageSettings = () => await this.add('Message_MaxAllowedSize', 5000, { type: 'int', public: true, + // i18nDescription references MESSAGE_PARSE_HARD_LIMIT (20000) — update both if the constant changes i18nDescription: 'Message_MaxAllowedSize_Description', }); await this.add('Message_AllowConvertLongMessagesToAttachment', true, { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8403a6c57deae..b0455b6270ecb 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3517,7 +3517,7 @@ "Message_KeepHistory": "Keep Per Message Editing History", "Message_MaxAll": "Maximum Channel Size for ALL Message", "Message_MaxAllowedSize": "Maximum Allowed Characters Per Message", - "Message_MaxAllowedSize_Description": "Maximum number of characters allowed per message. Messages exceeding this limit will be displayed as plain text without markdown parsing.", + "Message_MaxAllowedSize_Description": "Maximum number of characters allowed per message. Messages exceeding this limit will be displayed as plain text without markdown parsing. Note: markdown rendering is always disabled above 20,000 characters, regardless of this setting.", "Message_not_sent_try_again": "Message not sent. \nPlease try again", "Message_QuoteChainLimit": "Maximum Number of Chained Quotes", "Message_Read_Receipt_Enabled": "Show Read Receipts", From f331822d8a14ef96c5adbf3e6e1bdf6b8b3f231e Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Tue, 28 Apr 2026 21:57:45 -0300 Subject: [PATCH 13/17] deep clone attachments before normalization --- .../client/components/message/hooks/useNormalizedMessage.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index 8fda6978c0e46..c53b94d62c4a2 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -87,7 +87,11 @@ export const useNormalizedMessage = (message: TMessag }, ], attachments: message.attachments - ? normalizeAttachments(message.attachments, message.file?.name, message.file?.type) + ? normalizeAttachments( + message.attachments.map((a) => ({ ...a })), + message.file?.name, + message.file?.type, + ) : message.attachments, }; } From 0f75d73cdc0ceb46ad43fc9ff35b45f258a5a2a0 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Thu, 30 Apr 2026 17:25:57 -0300 Subject: [PATCH 14/17] change of approach: create a new setting to limit message parsing --- .../attachments/QuoteAttachment.spec.tsx | 8 ++++---- .../content/attachments/QuoteAttachment.tsx | 10 +++------- .../message/hooks/useMaxMarkdownParseLength.ts | 18 ++++++++++++++++++ .../message/hooks/useMaxMessageParseSize.ts | 18 ------------------ .../message/hooks/useNormalizedMessage.spec.ts | 4 ++-- .../message/hooks/useNormalizedMessage.ts | 9 ++++++--- .../message/variants/ThreadMessagePreview.tsx | 6 +++--- .../variants/room/RoomMessageContent.tsx | 6 +++--- .../variants/thread/ThreadMessageContent.tsx | 6 +++--- apps/meteor/client/lib/constants.ts | 1 - .../MessageList/hooks/useMessageBody.spec.ts | 4 ++-- .../room/MessageList/hooks/useMessageBody.tsx | 6 +++--- apps/meteor/server/settings/message.ts | 7 +++++-- packages/i18n/src/locales/en.i18n.json | 3 ++- 14 files changed, 54 insertions(+), 52 deletions(-) create mode 100644 apps/meteor/client/components/message/hooks/useMaxMarkdownParseLength.ts delete mode 100644 apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx index afbb090524ee0..a1862ae360d30 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.spec.tsx @@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react'; import { QuoteAttachment } from './QuoteAttachment'; -jest.mock('../../hooks/useMaxMessageParseSize', () => ({ - useMaxMessageParseSize: () => 100, +jest.mock('../../hooks/useMaxMarkdownParseLength', () => ({ + useMaxMarkdownParseLength: () => 100, })); jest.mock('@rocket.chat/ui-contexts', () => ({ @@ -28,12 +28,12 @@ const baseAttachment = { }; describe('QuoteAttachment', () => { - it('renders MessageContentBody when text length is within maxMessageParseSize', () => { + it('renders MessageContentBody when text length is within maxMarkdownParseLength', () => { render(); expect(screen.getByTestId('message-content-body')).toBeInTheDocument(); }); - it('renders plain text when text exceeds maxMessageParseSize', () => { + it('renders plain text when text exceeds maxMarkdownParseLength', () => { const longText = 'a'.repeat(101); const attachment = { ...baseAttachment, text: longText }; diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx index 271de136a2e8a..3d709046994bf 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx @@ -13,7 +13,7 @@ import AttachmentAuthorName from './structure/AttachmentAuthorName'; import AttachmentContent from './structure/AttachmentContent'; import AttachmentDetails from './structure/AttachmentDetails'; import AttachmentInner from './structure/AttachmentInner'; -import { useMaxMessageParseSize } from '../../hooks/useMaxMessageParseSize'; +import { useMaxMarkdownParseLength } from '../../hooks/useMaxMarkdownParseLength'; // TODO: remove this team collaboration const quoteStyles = css` @@ -39,7 +39,7 @@ type QuoteAttachmentProps = { export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElement => { const formatTime = useTimeAgo(); const displayAvatarPreference = useUserPreference('displayAvatars'); - const maxMessageParseSize = useMaxMessageParseSize(); + const maxMarkdownParseLength = useMaxMarkdownParseLength(); return ( <> @@ -73,11 +73,7 @@ export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElem )} - {attachment.text.length <= maxMessageParseSize && attachment.md ? ( - - ) : ( - attachment.text.substring(attachment.text.indexOf('\n') + 1) - )} + {attachment.text.length <= maxMarkdownParseLength && attachment.md ? : attachment.text} diff --git a/apps/meteor/client/components/message/hooks/useMaxMarkdownParseLength.ts b/apps/meteor/client/components/message/hooks/useMaxMarkdownParseLength.ts new file mode 100644 index 0000000000000..46514233c02da --- /dev/null +++ b/apps/meteor/client/components/message/hooks/useMaxMarkdownParseLength.ts @@ -0,0 +1,18 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +/** + * Returns the maximum number of characters a message can have for markdown parsing. + * Returns Infinity when the setting is 0 or negative, meaning the limit is disabled + * and all messages will be parsed regardless of length. + */ +export const useMaxMarkdownParseLength = (): number => { + const settingValue = useSetting('Message_MaxMarkdownParseLength', 0); + + return useMemo(() => { + if (typeof settingValue !== 'number' || settingValue <= 0) { + return Infinity; + } + return settingValue; + }, [settingValue]); +}; diff --git a/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts b/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts deleted file mode 100644 index 342a03aa0d7a5..0000000000000 --- a/apps/meteor/client/components/message/hooks/useMaxMessageParseSize.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useSetting } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -import { MESSAGE_PARSE_HARD_LIMIT } from '../../../lib/constants'; - -/** - * Returns the maximum allowed size for message parsing. - * Uses Math.min to ensure it never exceeds the hard limit to avoid performance issues. - * Always returns a number, never null or undefined. - */ -export const useMaxMessageParseSize = (): number => { - const settingValue = useSetting('Message_MaxAllowedSize', 5000); - - return useMemo(() => { - const maxSize = typeof settingValue === 'number' && settingValue > 0 ? settingValue : 5000; - return Math.min(maxSize, MESSAGE_PARSE_HARD_LIMIT); - }, [settingValue]); -}; diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts index b16ad0bd55699..1dc42bd8050f5 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.spec.ts @@ -32,7 +32,7 @@ describe('useNormalizedMessage', () => { mockParseMessageTextToAstMarkdown.mockClear(); }); - it('should skip parsing and returns PARAGRAPH node when msg exceeds maxMessageParseSize', () => { + it('should skip parsing and returns PARAGRAPH node when msg exceeds maxMarkdownParseLength', () => { const longMsg = 'a'.repeat(101); const message = { ...baseMessage, msg: longMsg }; @@ -47,7 +47,7 @@ describe('useNormalizedMessage', () => { ]); }); - it('should call parseMessageTextToAstMarkdown when msg is within maxMessageParseSize', () => { + it('should call parseMessageTextToAstMarkdown when msg is within maxMarkdownParseLength', () => { const message = { ...baseMessage, msg: 'Hello world' }; renderHook(() => useNormalizedMessage(message as any, 100)); diff --git a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index c53b94d62c4a2..c7aed05408419 100644 --- a/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -57,7 +57,10 @@ const normalizeAttachments = (attachments: MessageAttachment[], name?: string, t }); }; -export const useNormalizedMessage = (message: TMessage, maxMessageParseSize: number): MessageWithMdEnforced => { +export const useNormalizedMessage = ( + message: TMessage, + maxMarkdownParseLength: number, +): MessageWithMdEnforced => { const katex = useMessageListKatex(); const katexEnabled = !!katex; const customDomains = useAutoLinkDomains(); @@ -77,7 +80,7 @@ export const useNormalizedMessage = (message: TMessag }), }; - if (message.msg && message.msg.length > maxMessageParseSize) { + if (message.msg && message.msg.length > maxMarkdownParseLength) { return { ...message, md: [ @@ -115,6 +118,6 @@ export const useNormalizedMessage = (message: TMessag katex?.parenthesisSyntaxEnabled, message, autoTranslateOptions, - maxMessageParseSize, + maxMarkdownParseLength, ]); }; diff --git a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx index 1723cd0675948..d66dd065b9caa 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx @@ -31,7 +31,7 @@ import { useGoToThread } from '../../../views/room/hooks/useGoToThread'; import Emoji from '../../Emoji'; import { useShowTranslated } from '../list/MessageListContext'; import ThreadMessagePreviewBody from './threadPreview/ThreadMessagePreviewBody'; -import { useMaxMessageParseSize } from '../hooks/useMaxMessageParseSize'; +import { useMaxMarkdownParseLength } from '../hooks/useMaxMarkdownParseLength'; type ThreadMessagePreviewProps = { message: IThreadMessage; @@ -53,8 +53,8 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }: const messageType = parentMessage.isSuccess ? MessageTypes.getType(parentMessage.data) : null; - const maxMessageParseSize = useMaxMessageParseSize(); - const messageBody = useMessageBody(parentMessage.data, maxMessageParseSize); + const maxMarkdownParseLength = useMaxMarkdownParseLength(); + const messageBody = useMessageBody(parentMessage.data, maxMarkdownParseLength); const previewMessage = isParsedMessage(messageBody) ? { md: messageBody } : { msg: messageBody }; diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 6a6a7a05af46f..5409355765dea 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -18,7 +18,7 @@ import MessageActions from '../../content/MessageActions'; import Reactions from '../../content/Reactions'; import ThreadMetrics from '../../content/ThreadMetrics'; import UrlPreviews from '../../content/UrlPreviews'; -import { useMaxMessageParseSize } from '../../hooks/useMaxMessageParseSize'; +import { useMaxMarkdownParseLength } from '../../hooks/useMaxMarkdownParseLength'; import { useNormalizedMessage } from '../../hooks/useNormalizedMessage'; import { useOembedLayout } from '../../hooks/useOembedLayout'; import { useSubscriptionFromMessageQuery } from '../../hooks/useSubscriptionFromMessageQuery'; @@ -43,9 +43,9 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const chat = useChat(); const { t } = useTranslation(); - const maxMessageParseSize = useMaxMessageParseSize(); + const maxMarkdownParseLength = useMaxMarkdownParseLength(); - const normalizedMessage = useNormalizedMessage(message, maxMessageParseSize); + const normalizedMessage = useNormalizedMessage(message, maxMarkdownParseLength); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; const quotes = normalizedMessage?.attachments?.filter(isQuoteAttachment) || []; diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 1f26ec6cd6431..4003879b9d254 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -15,7 +15,7 @@ import Location from '../../content/Location'; import MessageActions from '../../content/MessageActions'; import Reactions from '../../content/Reactions'; import UrlPreviews from '../../content/UrlPreviews'; -import { useMaxMessageParseSize } from '../../hooks/useMaxMessageParseSize'; +import { useMaxMarkdownParseLength } from '../../hooks/useMaxMarkdownParseLength'; import { useNormalizedMessage } from '../../hooks/useNormalizedMessage'; import { useOembedLayout } from '../../hooks/useOembedLayout'; import { useSubscriptionFromMessageQuery } from '../../hooks/useSubscriptionFromMessageQuery'; @@ -34,11 +34,11 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const uid = useUserId(); const { enabled: readReceiptEnabled } = useMessageListReadReceipts(); const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; - const maxMessageParseSize = useMaxMessageParseSize(); + const maxMarkdownParseLength = useMaxMarkdownParseLength(); const { t } = useTranslation(); - const normalizedMessage = useNormalizedMessage(message, maxMessageParseSize); + const normalizedMessage = useNormalizedMessage(message, maxMarkdownParseLength); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; diff --git a/apps/meteor/client/lib/constants.ts b/apps/meteor/client/lib/constants.ts index cd57c13d92078..70cae5deff224 100644 --- a/apps/meteor/client/lib/constants.ts +++ b/apps/meteor/client/lib/constants.ts @@ -3,4 +3,3 @@ export const BIO_TEXT_MAX_LENGTH = 260; export const VIDEOCONF_STACK_MAX_USERS = 6; export const NAVIGATION_REGION_ID = 'navigation-region'; export const MAX_FILE_SIZE_PREVIEW = 10485760; // 10MB -export const MESSAGE_PARSE_HARD_LIMIT = 20000; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.spec.ts b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.spec.ts index 6890a3a7f4f0e..e792260674d9d 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.spec.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.spec.ts @@ -32,7 +32,7 @@ describe('useMessageBody', () => { mockParseMessageTextToAstMarkdown.mockClear(); }); - it('should return raw msg and skips parsing when msg exceeds maxMessageParseSize', () => { + it('should return raw msg and skips parsing when msg exceeds maxMarkdownParseLength', () => { const longMsg = 'a'.repeat(101); const message = { ...baseMessage, msg: longMsg, md: [{ type: 'PARAGRAPH', value: [] }] }; @@ -42,7 +42,7 @@ describe('useMessageBody', () => { expect(mockParseMessageTextToAstMarkdown).not.toHaveBeenCalled(); }); - it('should call parser when message has md and is within maxMessageParseSize', () => { + it('should call parser when message has md and is within maxMarkdownParseLength', () => { const md = [{ type: 'PARAGRAPH', value: [] }]; const message = { ...baseMessage, msg: 'Hello world', md }; mockParseMessageTextToAstMarkdown.mockReturnValue({ ...message, md }); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx index 09c1201a9cced..01f41f0d2fcd9 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx @@ -6,7 +6,7 @@ import { useAutoLinkDomains } from './useAutoLinkDomains'; import { useMessageListAutoTranslate } from '../../../../components/message/list/MessageListContext'; import { parseMessageTextToAstMarkdown } from '../../../../lib/parseMessageTextToAstMarkdown'; -export const useMessageBody = (message: IMessage | undefined, maxMessageParseSize: number): string | Root => { +export const useMessageBody = (message: IMessage | undefined, maxMarkdownParseLength: number): string | Root => { const autoTranslateOptions = useMessageListAutoTranslate(); const customDomains = useAutoLinkDomains(); @@ -15,7 +15,7 @@ export const useMessageBody = (message: IMessage | undefined, maxMessageParseSiz return ''; } - if (message.msg && message.msg.length > maxMessageParseSize) { + if (message.msg && message.msg.length > maxMarkdownParseLength) { return message.msg; } @@ -43,5 +43,5 @@ export const useMessageBody = (message: IMessage | undefined, maxMessageParseSiz } return ''; - }, [message, customDomains, autoTranslateOptions, maxMessageParseSize]); + }, [message, customDomains, autoTranslateOptions, maxMarkdownParseLength]); }; diff --git a/apps/meteor/server/settings/message.ts b/apps/meteor/server/settings/message.ts index 57923586e658f..b92a2cbeb9b06 100644 --- a/apps/meteor/server/settings/message.ts +++ b/apps/meteor/server/settings/message.ts @@ -168,8 +168,11 @@ export const createMessageSettings = () => await this.add('Message_MaxAllowedSize', 5000, { type: 'int', public: true, - // i18nDescription references MESSAGE_PARSE_HARD_LIMIT (20000) — update both if the constant changes - i18nDescription: 'Message_MaxAllowedSize_Description', + }); + await this.add('Message_MaxMarkdownParseLength', 0, { + type: 'int', + public: true, + i18nDescription: 'Message_MaxMarkdownParseLength_Description', }); await this.add('Message_AllowConvertLongMessagesToAttachment', true, { type: 'boolean', diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index b0455b6270ecb..cd967c6867fb9 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3517,7 +3517,8 @@ "Message_KeepHistory": "Keep Per Message Editing History", "Message_MaxAll": "Maximum Channel Size for ALL Message", "Message_MaxAllowedSize": "Maximum Allowed Characters Per Message", - "Message_MaxAllowedSize_Description": "Maximum number of characters allowed per message. Messages exceeding this limit will be displayed as plain text without markdown parsing. Note: markdown rendering is always disabled above 20,000 characters, regardless of this setting.", + "Message_MaxMarkdownParseLength": "Maximum Markdown Parsing Characters", + "Message_MaxMarkdownParseLength_Description": "Maximum number of characters per message to be Markdown parsed. Messages longer than this will be rendered as plain text. Set to 0 to disable this limit. Warning: High values may significantly impact client-side performance.", "Message_not_sent_try_again": "Message not sent. \nPlease try again", "Message_QuoteChainLimit": "Maximum Number of Chained Quotes", "Message_Read_Receipt_Enabled": "Show Read Receipts", From 32d6517e872add9b8566457219713f7559e728a7 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli Date: Thu, 30 Apr 2026 18:07:05 -0300 Subject: [PATCH 15/17] update changeset --- .changeset/cool-flowers-stop.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/cool-flowers-stop.md b/.changeset/cool-flowers-stop.md index c45b3306ffc0c..768412daec0ba 100644 --- a/.changeset/cool-flowers-stop.md +++ b/.changeset/cool-flowers-stop.md @@ -1,6 +1,6 @@ --- -'@rocket.chat/i18n': patch -'@rocket.chat/meteor': patch +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor --- -Fixes workspace freezing when rendering massive markdown messages +Adds `Message_MaxMarkdownParseLength` setting to limit the number of characters processed by the message parser. From dd217aa2ce6d248d5085a7c272cd6b2c7a9c5abb Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli <84046180+nazabucciarelli@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:55:03 -0300 Subject: [PATCH 16/17] improve i18n setting description Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/i18n/src/locales/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index cd967c6867fb9..8c2712d81f9c2 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3518,7 +3518,7 @@ "Message_MaxAll": "Maximum Channel Size for ALL Message", "Message_MaxAllowedSize": "Maximum Allowed Characters Per Message", "Message_MaxMarkdownParseLength": "Maximum Markdown Parsing Characters", - "Message_MaxMarkdownParseLength_Description": "Maximum number of characters per message to be Markdown parsed. Messages longer than this will be rendered as plain text. Set to 0 to disable this limit. Warning: High values may significantly impact client-side performance.", + "Message_MaxMarkdownParseLength_Description": "Maximum number of characters per message to be parsed as Markdown. Messages longer than this will be rendered as plain text. Set to 0 to disable this limit. Warning: High values may significantly impact client-side performance.", "Message_not_sent_try_again": "Message not sent. \nPlease try again", "Message_QuoteChainLimit": "Maximum Number of Chained Quotes", "Message_Read_Receipt_Enabled": "Show Read Receipts", From 27efcb34afff2ae126f6c948d5fd27df60377276 Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli <84046180+nazabucciarelli@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:12:31 -0300 Subject: [PATCH 17/17] improve changeset description --- .changeset/cool-flowers-stop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cool-flowers-stop.md b/.changeset/cool-flowers-stop.md index 768412daec0ba..c680f8e457be7 100644 --- a/.changeset/cool-flowers-stop.md +++ b/.changeset/cool-flowers-stop.md @@ -3,4 +3,4 @@ '@rocket.chat/meteor': minor --- -Adds `Message_MaxMarkdownParseLength` setting to limit the number of characters processed by the message parser. +Adds `Message_MaxMarkdownParseLength` setting to limit the number of characters processed by the Markdown parser.