diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index f88057e49..5f1fc91f6 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -57,6 +57,7 @@ export function TextInput({ const theme = useTheme() const [autocompletePrefix, setAutocompletePrefix] = useState('') const prevLength = useRef(richtext.length) + const prevText = useRef(richtext.text) useImperativeHandle(ref, () => ({ focus: () => textInput.current?.focus(), @@ -73,6 +74,44 @@ export function TextInput({ async (newText: string) => { const mayBePaste = newText.length > prevLength.current + 1 + // Check if this is a paste over selected text with a URL + // NOTE: onChangeText happens before onSelectionChange, so textInputSelection.current + // still contains the selection from before the paste + if ( + mayBePaste && + textInputSelection.current.start !== textInputSelection.current.end + ) { + const selectionStart = textInputSelection.current.start + const selectionEnd = textInputSelection.current.end + const selectedText = prevText.current.substring( + selectionStart, + selectionEnd, + ) + + // Calculate what was pasted + const beforeSelection = prevText.current.substring(0, selectionStart) + const afterSelection = prevText.current.substring(selectionEnd) + const expectedLength = beforeSelection.length + afterSelection.length + const pastedLength = newText.length - expectedLength + + if (pastedLength > 0 && selectedText.length > 0) { + const pastedText = newText.substring( + selectionStart, + selectionStart + pastedLength, + ) + + // Check if pasted text is a URL + const urlPattern = + /^(?:(?:(?:https?|ftp):)?\/\/)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i + + if (urlPattern.test(pastedText.trim())) { + // Create markdown-style link: [selectedText](url) + const markdownLink = `[${selectedText}](${pastedText.trim()})` + newText = beforeSelection + markdownLink + afterSelection + } + } + } + const newRt = new RichText({text: newText}) newRt.detectFacetsWithoutResolution() @@ -170,6 +209,7 @@ export function TextInput({ onNewLink(suggestedUri) } prevLength.current = newText.length + prevText.current = newText }, [setRichText, autocompletePrefix, onPhotoPasted, onNewLink], ) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 58d5392d0..a7d766fa9 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -183,6 +183,41 @@ export function TextInput({ let preventDefault = false if (clipboardData) { + // Check if text is selected and pasted content is a URL + const selection = view.state.selection + const hasSelection = !selection.empty + + if (hasSelection && clipboardData.types.includes('text/plain')) { + const pastedText = clipboardData.getData('text/plain').trim() + const urlPattern = + /^(?:(?:(?:https?|ftp):)?\/\/)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i + + if (urlPattern.test(pastedText)) { + const selectedText = view.state.doc.textBetween( + selection.from, + selection.to, + '', + ) + + if (selectedText) { + // Create markdown-style link: [selectedText](url) + const markdownLink = `[${selectedText}](${pastedText})` + const {from, to} = selection + + view.dispatch( + view.state.tr.replaceWith( + from, + to, + view.state.schema.text(markdownLink), + ), + ) + + preventDefault = true + return true + } + } + } + if (clipboardData.types.includes('text/html')) { // Rich-text formatting is pasted, try retrieving plain text const text = clipboardData.getData('text/plain')