From 7d56481abae654f2ddf52e377dec76a736fbe3fc Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 14 Sep 2026 14:53:53 +0200 Subject: [PATCH] fix(app): copy selected message text without markdown (#2759) --- .../channel/components/UserMessage.tsx | 16 ++++++++-- .../renderers/common/BlockElements.tsx | 2 +- .../messaging/utils/MessageCopyTextUtils.ts | 31 +++++++++++++------ .../utils/MessageSelectionCopyUtils.test.ts | 2 +- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/fluxer_app/src/features/channel/components/UserMessage.tsx b/fluxer_app/src/features/channel/components/UserMessage.tsx index 44def293f..4f578991a 100644 --- a/fluxer_app/src/features/channel/components/UserMessage.tsx +++ b/fluxer_app/src/features/channel/components/UserMessage.tsx @@ -27,6 +27,7 @@ import {parse} from '@app/features/messaging/components/markdown/renderers'; import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes'; import MessageEdit from '@app/features/messaging/state/MessageEdit'; import {hasStyleableMessageText} from '@app/features/messaging/utils/FailedMessageDisplayUtils'; +import {buildMessageContentCopyText} from '@app/features/messaging/utils/MessageCopyTextUtils'; import { buildExistingAttachmentEditReferences, canSubmitEmptyMessageEdit, @@ -140,6 +141,16 @@ export const UserMessage = observer(() => { }), [message.id, message.channelId, message.mentionChannels], ); + const contentCopyText = useMemo( + () => + buildMessageContentCopyText(astNodes, { + channelId: message.channelId, + messageId: message.id, + mentionChannels: message.mentionChannels, + i18n, + }), + [astNodes, message.id, message.channelId, message.mentionChannels, i18n.locale], + ); const shouldHideContent = UserSettings.getRenderEmbeds() && message.embeds.length > 0 && @@ -281,7 +292,7 @@ export const UserMessage = observer(() => { className={clsx(markupStyles.markup)} data-search-highlight-scope="message" data-flx="channel.user-message.render-message-content.div" - {...messageContentCopyBlockProps(message.content)} + {...messageContentCopyBlockProps(contentCopyText)} > { shouldShowEditingInput, shouldHideContent, markdownOptions, + contentCopyText, message, message.content, message.id, @@ -415,7 +427,7 @@ export const UserMessage = observer(() => { className={clsx(markupStyles.markup)} data-search-highlight-scope="message" data-flx="channel.user-message.div" - {...messageContentCopyBlockProps(message.content)} + {...messageContentCopyBlockProps(contentCopyText)} > ): React.ReactElement { const copyText = renderTableNodeToMarkdown(node, { channelId: options.channelId, - preserveMarkdown: true, + preserveMarkdown: false, includeEmojiNames: true, i18n: options.i18n, }); diff --git a/fluxer_app/src/features/messaging/utils/MessageCopyTextUtils.ts b/fluxer_app/src/features/messaging/utils/MessageCopyTextUtils.ts index ac1b74a29..2e71584f5 100644 --- a/fluxer_app/src/features/messaging/utils/MessageCopyTextUtils.ts +++ b/fluxer_app/src/features/messaging/utils/MessageCopyTextUtils.ts @@ -3,7 +3,12 @@ import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes'; import type {Message} from '@app/features/messaging/models/MessagingMessage'; import {getParserFlagsForContext} from '@app/features/messaging/utils/markdown/MarkdownParserFlags'; -import {parseAndRenderToPlaintext} from '@app/features/messaging/utils/markdown/Plaintext'; +import { + type PlaintextRenderOptions, + parseAndRenderToPlaintext, + renderAstToPlaintext, +} from '@app/features/messaging/utils/markdown/Plaintext'; +import type {Node} from '@app/features/messaging/utils/markdown/parser/Nodes'; import * as DateUtils from '@app/features/user/utils/DateFormatting'; import {MessageEmbedTypes} from '@fluxer/constants/src/ChannelConstants'; import type {MessageEmbed} from '@fluxer/schema/src/domains/message/EmbedSchemas'; @@ -102,6 +107,17 @@ function buildOmittedEmbedUrls(content?: string | null, renderedContent?: string return urls; } +function createPlaintextCopyOptions(context: MarkdownCopyContext): PlaintextRenderOptions { + return { + channelId: context.channelId, + preserveMarkdown: false, + includeEmojiNames: true, + includeLinkUrls: true, + mentionChannels: context.mentionChannels, + i18n: context.i18n, + }; +} + function renderMarkdownCopyText( content: string | undefined | null, parserFlags: number, @@ -110,14 +126,11 @@ function renderMarkdownCopyText( if (!content) { return ''; } - return parseAndRenderToPlaintext(content, parserFlags, { - channelId: context.channelId, - preserveMarkdown: false, - includeEmojiNames: true, - includeLinkUrls: true, - mentionChannels: context.mentionChannels, - i18n: context.i18n, - }); + return parseAndRenderToPlaintext(content, parserFlags, createPlaintextCopyOptions(context)); +} + +export function buildMessageContentCopyText(nodes: Array, context: MarkdownCopyContext): string { + return normaliseCopyBlock(renderAstToPlaintext(nodes, createPlaintextCopyOptions(context))); } function buildAttachmentCopyText(attachment: MessageAttachment): string { diff --git a/fluxer_app/src/features/messaging/utils/MessageSelectionCopyUtils.test.ts b/fluxer_app/src/features/messaging/utils/MessageSelectionCopyUtils.test.ts index d92bef4b1..604b2b3b7 100644 --- a/fluxer_app/src/features/messaging/utils/MessageSelectionCopyUtils.test.ts +++ b/fluxer_app/src/features/messaging/utils/MessageSelectionCopyUtils.test.ts @@ -44,7 +44,7 @@ describe('Message selection copy utils', () => { expect(buildMessageSelectionCopyTextForRange({rootElement: root, selectionRange: range})).toBe(tableCopyText); }); it('does not duplicate a block message header when a bot badge is selected with the body', () => { - const messageContent = '## App Canary Deployed\n\nVersion: `2026.519.3`\nImage: `2026.519.3`'; + const messageContent = 'App Canary Deployed\n\nVersion: 2026.519.3\nImage: 2026.519.3'; document.body.innerHTML = [ '
', '
',