From 2083eaddd646c70376feea0b195133bba1c397e5 Mon Sep 17 00:00:00 2001 From: Hampus Date: Wed, 19 Aug 2026 17:19:39 +0200 Subject: [PATCH] feat(app): wrap pasted links in every composer that allows masked links (#1755) --- .../components/useChannelComposerPaste.tsx | 20 +----------- .../lexical/composer/ComposerClipboard.ts | 32 +++++++++++++++++++ .../composer/ComposerLinkPaste.test.ts | 17 ++++++++++ .../lexical/composer/LexicalComposerInput.tsx | 4 +++ 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/fluxer_app/src/features/channel/components/useChannelComposerPaste.tsx b/fluxer_app/src/features/channel/components/useChannelComposerPaste.tsx index c66e1e2f1..1f7f56f91 100644 --- a/fluxer_app/src/features/channel/components/useChannelComposerPaste.tsx +++ b/fluxer_app/src/features/channel/components/useChannelComposerPaste.tsx @@ -13,7 +13,6 @@ import { parseComposerClipboardSlice, } from '@app/features/lexical/composer/ComposerClipboard'; import type {ComposerHandle} from '@app/features/lexical/composer/ComposerHandle'; -import {resolvePastedLinkInsertion} from '@app/features/lexical/composer/ComposerLinkPaste'; import {showAttachmentPermissionDeniedModal} from '@app/features/messaging/components/alerts/AttachmentPermissionDeniedModal'; import {FileSizeTooLargeModal} from '@app/features/messaging/components/alerts/FileSizeTooLargeModal'; import {TooManyAttachmentsModal} from '@app/features/messaging/components/alerts/TooManyAttachmentsModal'; @@ -33,7 +32,7 @@ import {modal} from '@app/features/ui/commands/ModalCommands'; import ContextMenuState from '@app/features/ui/state/ContextMenu'; import KeyboardMode from '@app/features/ui/state/KeyboardMode'; import Users from '@app/features/user/state/Users'; -import {$getSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, PASTE_COMMAND} from 'lexical'; +import {COMMAND_PRIORITY_HIGH, PASTE_COMMAND} from 'lexical'; import type React from 'react'; import {useCallback, useEffect} from 'react'; @@ -69,19 +68,6 @@ function createComposerPasteSlice(pastedText: string, segments: ReadonlyArray void { return mergeRegister( + editor.registerCommand( + PASTE_COMMAND, + (event) => { + if (!state.isEditable() || (state.getMarkdownParserFlags() & ParserFlags.ALLOW_MASKED_LINKS) === 0) { + return false; + } + const clipboardEvent = getClipboardEvent(event); + if (clipboardEvent == null || clipboardEvent.clipboardData == null) { + return false; + } + const selection = $getSelection(); + if (!$isRangeSelection(selection) || selection.isCollapsed()) { + return false; + } + const replacement = resolvePastedLinkInsertion( + clipboardEvent.clipboardData.getData('text/plain'), + selection.getTextContent(), + ); + if (replacement === null) { + return false; + } + selection.insertText(replacement); + $addUpdateTag(PASTE_TAG); + clipboardEvent.preventDefault(); + return true; + }, + COMMAND_PRIORITY_CRITICAL, + ), editor.registerCommand( COPY_COMMAND, (event) => { diff --git a/fluxer_app/src/features/lexical/composer/ComposerLinkPaste.test.ts b/fluxer_app/src/features/lexical/composer/ComposerLinkPaste.test.ts index bd4d6ad2e..6595d7a61 100644 --- a/fluxer_app/src/features/lexical/composer/ComposerLinkPaste.test.ts +++ b/fluxer_app/src/features/lexical/composer/ComposerLinkPaste.test.ts @@ -6,6 +6,10 @@ import { parsePastedUrl, resolvePastedLinkInsertion, } from '@app/features/lexical/composer/ComposerLinkPaste'; +import {DEFAULT_COMPOSER_MARKDOWN_FLAGS} from '@app/features/lexical/composer/markdownSpans'; +import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes'; +import {getParserFlagsForContext} from '@app/features/messaging/utils/markdown/MarkdownParserFlags'; +import {ParserFlags} from '@app/features/messaging/utils/markdown/parser/Enums'; import {describe, expect, it} from 'vitest'; describe('parsePastedUrl', () => { @@ -87,3 +91,16 @@ describe('resolvePastedLinkInsertion', () => { expect(resolvePastedLinkInsertion('javascript:alert(1)', 'click me')).toBeNull(); }); }); + +describe('masked link support across composer surfaces', () => { + const surfaces: Array<[string, number]> = [ + ['channel composer', DEFAULT_COMPOSER_MARKDOWN_FLAGS], + ['message edit / forward', getParserFlagsForContext(MarkdownContext.STANDARD_WITH_JUMBO)], + ['channel topic', getParserFlagsForContext(MarkdownContext.STANDARD_WITHOUT_JUMBO)], + ['user bio', getParserFlagsForContext(MarkdownContext.RESTRICTED_USER_BIO)], + ]; + + it.each(surfaces)('%s allows masked links, so pasting a link must wrap', (_name, flags) => { + expect(flags & ParserFlags.ALLOW_MASKED_LINKS).not.toBe(0); + }); +}); diff --git a/fluxer_app/src/features/lexical/composer/LexicalComposerInput.tsx b/fluxer_app/src/features/lexical/composer/LexicalComposerInput.tsx index 063424903..b1f922aca 100644 --- a/fluxer_app/src/features/lexical/composer/LexicalComposerInput.tsx +++ b/fluxer_app/src/features/lexical/composer/LexicalComposerInput.tsx @@ -33,6 +33,7 @@ import { $wrapComposerSelection, } from '@app/features/lexical/composer/composerOffsets'; import styles from '@app/features/lexical/composer/LexicalMessageComposer.module.css'; +import {DEFAULT_COMPOSER_MARKDOWN_FLAGS} from '@app/features/lexical/composer/markdownSpans'; import {ComposerCommandNode} from '@app/features/lexical/composer/nodes/ComposerCommandNode'; import {ComposerCustomEmojiNode} from '@app/features/lexical/composer/nodes/ComposerCustomEmojiNode'; import {ComposerMentionNode} from '@app/features/lexical/composer/nodes/ComposerMentionNode'; @@ -263,6 +264,8 @@ const ComposerInner = ({ submitOnEnterRef.current = submitOnEnter; const plainTextRef = useRef(plainText); plainTextRef.current = plainText; + const markdownParserFlagsRef = useRef(markdownParserFlags ?? DEFAULT_COMPOSER_MARKDOWN_FLAGS); + markdownParserFlagsRef.current = markdownParserFlags ?? DEFAULT_COMPOSER_MARKDOWN_FLAGS; const disabledRef = useRef(disabled); disabledRef.current = disabled; const onSlashCommandStateChangeRef = useRef(onSlashCommandStateChange); @@ -607,6 +610,7 @@ const ComposerInner = ({ registerComposerClipboardCommands(editor, { getPlainText: () => plainTextRef.current, isEditable: () => !disabledRef.current && editor.isEditable(), + getMarkdownParserFlags: () => markdownParserFlagsRef.current, }), [editor], );