Bluesky app fork with some witchin' additions ๐Ÿ’ซ witchsky.app
bluesky fork client

feat: paste link on text for masked link!! ๐ŸŽ†

todo: put all urlPattern matches into one thing for importing (like all uris-doesn't this already exist in the codebase?)

xan.lol 32d0a536 4f10f44e

verified
+75
+40
src/view/com/composer/text-input/TextInput.tsx
··· 57 57 const theme = useTheme() 58 58 const [autocompletePrefix, setAutocompletePrefix] = useState('') 59 59 const prevLength = useRef(richtext.length) 60 + const prevText = useRef(richtext.text) 60 61 61 62 useImperativeHandle(ref, () => ({ 62 63 focus: () => textInput.current?.focus(), ··· 72 73 const onChangeText = useCallback( 73 74 async (newText: string) => { 74 75 const mayBePaste = newText.length > prevLength.current + 1 76 + 77 + // Check if this is a paste over selected text with a URL 78 + // NOTE: onChangeText happens before onSelectionChange, so textInputSelection.current 79 + // still contains the selection from before the paste 80 + if ( 81 + mayBePaste && 82 + textInputSelection.current.start !== textInputSelection.current.end 83 + ) { 84 + const selectionStart = textInputSelection.current.start 85 + const selectionEnd = textInputSelection.current.end 86 + const selectedText = prevText.current.substring( 87 + selectionStart, 88 + selectionEnd, 89 + ) 90 + 91 + // Calculate what was pasted 92 + const beforeSelection = prevText.current.substring(0, selectionStart) 93 + const afterSelection = prevText.current.substring(selectionEnd) 94 + const expectedLength = beforeSelection.length + afterSelection.length 95 + const pastedLength = newText.length - expectedLength 96 + 97 + if (pastedLength > 0 && selectedText.length > 0) { 98 + const pastedText = newText.substring( 99 + selectionStart, 100 + selectionStart + pastedLength, 101 + ) 102 + 103 + // Check if pasted text is a URL 104 + const urlPattern = 105 + /^(?:(?:(?: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 106 + 107 + if (urlPattern.test(pastedText.trim())) { 108 + // Create markdown-style link: [selectedText](url) 109 + const markdownLink = `[${selectedText}](${pastedText.trim()})` 110 + newText = beforeSelection + markdownLink + afterSelection 111 + } 112 + } 113 + } 75 114 76 115 const newRt = new RichText({text: newText}) 77 116 newRt.detectFacetsWithoutResolution() ··· 170 209 onNewLink(suggestedUri) 171 210 } 172 211 prevLength.current = newText.length 212 + prevText.current = newText 173 213 }, 174 214 [setRichText, autocompletePrefix, onPhotoPasted, onNewLink], 175 215 )
+35
src/view/com/composer/text-input/TextInput.web.tsx
··· 183 183 let preventDefault = false 184 184 185 185 if (clipboardData) { 186 + // Check if text is selected and pasted content is a URL 187 + const selection = view.state.selection 188 + const hasSelection = !selection.empty 189 + 190 + if (hasSelection && clipboardData.types.includes('text/plain')) { 191 + const pastedText = clipboardData.getData('text/plain').trim() 192 + const urlPattern = 193 + /^(?:(?:(?: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 194 + 195 + if (urlPattern.test(pastedText)) { 196 + const selectedText = view.state.doc.textBetween( 197 + selection.from, 198 + selection.to, 199 + '', 200 + ) 201 + 202 + if (selectedText) { 203 + // Create markdown-style link: [selectedText](url) 204 + const markdownLink = `[${selectedText}](${pastedText})` 205 + const {from, to} = selection 206 + 207 + view.dispatch( 208 + view.state.tr.replaceWith( 209 + from, 210 + to, 211 + view.state.schema.text(markdownLink), 212 + ), 213 + ) 214 + 215 + preventDefault = true 216 + return true 217 + } 218 + } 219 + } 220 + 186 221 if (clipboardData.types.includes('text/html')) { 187 222 // Rich-text formatting is pasted, try retrieving plain text 188 223 const text = clipboardData.getData('text/plain')