mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rewrite the link detection (#3687)

* Rewrite the link detection

* Handle parens and colons

authored by danabra.mov and committed by

GitHub 8ec3d8c7 b3df0b17

+110 -87
+16 -20
src/view/com/composer/text-input/TextInput.tsx
··· 28 28 import {useTheme} from 'lib/ThemeContext' 29 29 import {isIOS} from 'platform/detection' 30 30 import { 31 - addLinkCardIfNecessary, 32 - findIndexInText, 31 + LinkFacetMatch, 32 + suggestLinkCardUri, 33 33 } from 'view/com/composer/text-input/text-input-util' 34 34 import {Text} from 'view/com/util/text/Text' 35 35 import {Autocomplete} from './mobile/Autocomplete' ··· 73 73 const theme = useTheme() 74 74 const [autocompletePrefix, setAutocompletePrefix] = useState('') 75 75 const prevLength = React.useRef(richtext.length) 76 - const prevAddedLinks = useRef(new Set<string>()) 77 76 78 77 React.useImperativeHandle(ref, () => ({ 79 78 focus: () => textInput.current?.focus(), ··· 83 82 getCursorPosition: () => undefined, // Not implemented on native 84 83 })) 85 84 85 + const pastSuggestedUris = useRef(new Set<string>()) 86 + const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>()) 86 87 const onChangeText = useCallback( 87 88 (newText: string) => { 88 89 /* ··· 112 113 setAutocompletePrefix('') 113 114 } 114 115 116 + const nextDetectedUris = new Map<string, LinkFacetMatch>() 115 117 if (newRt.facets) { 116 118 for (const facet of newRt.facets) { 117 119 for (const feature of facet.features) { ··· 130 132 onPhotoPasted(res.path) 131 133 } 132 134 } else { 133 - const cursorLocation = textInputSelection.current.end 134 - 135 - addLinkCardIfNecessary({ 136 - uri: feature.uri, 137 - newText, 138 - cursorLocation, 139 - mayBePaste, 140 - onNewLink, 141 - prevAddedLinks: prevAddedLinks.current, 142 - }) 135 + nextDetectedUris.set(feature.uri, {facet, rt: newRt}) 143 136 } 144 137 } 145 138 } 146 139 } 147 140 } 148 - 149 - for (const uri of prevAddedLinks.current.keys()) { 150 - if (findIndexInText(uri, newText) === -1) { 151 - prevAddedLinks.current.delete(uri) 152 - } 141 + const suggestedUri = suggestLinkCardUri( 142 + mayBePaste, 143 + nextDetectedUris, 144 + prevDetectedUris.current, 145 + pastSuggestedUris.current, 146 + ) 147 + prevDetectedUris.current = nextDetectedUris 148 + if (suggestedUri) { 149 + onNewLink(suggestedUri) 153 150 } 154 - 155 151 prevLength.current = newText.length 156 152 }, 1) 157 153 }, 158 - [setRichText, autocompletePrefix, onPhotoPasted, prevAddedLinks, onNewLink], 154 + [setRichText, autocompletePrefix, onPhotoPasted, onNewLink], 159 155 ) 160 156 161 157 const onPaste = useCallback(
+16 -22
src/view/com/composer/text-input/TextInput.web.tsx
··· 19 19 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' 20 20 import {blobToDataUri, isUriImage} from 'lib/media/util' 21 21 import { 22 - addLinkCardIfNecessary, 23 - findIndexInText, 22 + LinkFacetMatch, 23 + suggestLinkCardUri, 24 24 } from 'view/com/composer/text-input/text-input-util' 25 25 import {Portal} from '#/components/Portal' 26 26 import {Text} from '../../util/text/Text' ··· 61 61 ref, 62 62 ) { 63 63 const autocomplete = useActorAutocompleteFn() 64 - const prevAddedLinks = useRef(new Set<string>()) 65 - 66 64 const pal = usePalette('default') 67 65 const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') 68 66 ··· 143 141 } 144 142 }, [setIsDropping]) 145 143 144 + const pastSuggestedUris = useRef(new Set<string>()) 145 + const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>()) 146 146 const editor = useEditor( 147 147 { 148 148 extensions, ··· 185 185 onUpdate({editor: editorProp}) { 186 186 const json = editorProp.getJSON() 187 187 const newText = editorJsonToText(json) 188 - const mayBePaste = window.event?.type === 'paste' 188 + const isPaste = window.event?.type === 'paste' 189 189 190 190 const newRt = new RichText({text: newText}) 191 191 newRt.detectFacetsWithoutResolution() 192 192 setRichText(newRt) 193 193 194 + const nextDetectedUris = new Map<string, LinkFacetMatch>() 194 195 if (newRt.facets) { 195 196 for (const facet of newRt.facets) { 196 197 for (const feature of facet.features) { 197 198 if (AppBskyRichtextFacet.isLink(feature)) { 198 - // The TipTap editor shows the position as being one character ahead, as if the start index is 1. 199 - // Subtracting 1 from the pos gives us the same behavior as the native impl. 200 - let cursorLocation = editor?.state.selection.$anchor.pos ?? 1 201 - cursorLocation -= 1 202 - 203 - addLinkCardIfNecessary({ 204 - uri: feature.uri, 205 - newText, 206 - cursorLocation, 207 - mayBePaste, 208 - onNewLink, 209 - prevAddedLinks: prevAddedLinks.current, 210 - }) 199 + nextDetectedUris.set(feature.uri, {facet, rt: newRt}) 211 200 } 212 201 } 213 202 } 214 203 } 215 204 216 - for (const uri of prevAddedLinks.current.keys()) { 217 - if (findIndexInText(uri, newText) === -1) { 218 - prevAddedLinks.current.delete(uri) 219 - } 205 + const suggestedUri = suggestLinkCardUri( 206 + isPaste, 207 + nextDetectedUris, 208 + prevDetectedUris.current, 209 + pastSuggestedUris.current, 210 + ) 211 + prevDetectedUris.current = nextDetectedUris 212 + if (suggestedUri) { 213 + onNewLink(suggestedUri) 220 214 } 221 215 }, 222 216 },
+78 -45
src/view/com/composer/text-input/text-input-util.ts
··· 1 - export function addLinkCardIfNecessary({ 2 - uri, 3 - newText, 4 - cursorLocation, 5 - mayBePaste, 6 - onNewLink, 7 - prevAddedLinks, 8 - }: { 9 - uri: string 10 - newText: string 11 - cursorLocation: number 12 - mayBePaste: boolean 13 - onNewLink: (uri: string) => void 14 - prevAddedLinks: Set<string> 15 - }) { 16 - // It would be cool if we could just use facet.index.byteEnd, but you know... *upside down smiley* 17 - const lastCharacterPosition = findIndexInText(uri, newText) + uri.length 1 + import {AppBskyRichtextFacet, RichText} from '@atproto/api' 18 2 19 - // If the text being added is not from a paste, then we should only check if the cursor is one 20 - // position ahead of the last character. However, if it is a paste we need to check both if it's 21 - // the same position _or_ one position ahead. That is because iOS will add a space after a paste if 22 - // pasting into the middle of a sentence! 23 - const cursorLocationIsOkay = 24 - cursorLocation === lastCharacterPosition + 1 || mayBePaste 3 + export type LinkFacetMatch = { 4 + rt: RichText 5 + facet: AppBskyRichtextFacet.Main 6 + } 25 7 26 - // Checking previouslyAddedLinks keeps a card from getting added over and over i.e. 27 - // Link card added -> Remove link card -> Press back space -> Press space -> Link card added -> and so on 8 + export function suggestLinkCardUri( 9 + mayBePaste: boolean, 10 + nextDetectedUris: Map<string, LinkFacetMatch>, 11 + prevDetectedUris: Map<string, LinkFacetMatch>, 12 + pastSuggestedUris: Set<string>, 13 + ): string | undefined { 14 + const suggestedUris = new Set<string>() 15 + for (const [uri, nextMatch] of nextDetectedUris) { 16 + if (!isValidUrlAndDomain(uri)) { 17 + continue 18 + } 19 + if (pastSuggestedUris.has(uri)) { 20 + // Don't suggest already added or already dismissed link cards. 21 + continue 22 + } 23 + if (mayBePaste) { 24 + // Immediately add the pasted link without waiting to type more. 25 + suggestedUris.add(uri) 26 + continue 27 + } 28 + const prevMatch = prevDetectedUris.get(uri) 29 + if (!prevMatch) { 30 + // If the same exact link wasn't already detected during the last keystroke, 31 + // it means you're probably still typing it. Disregard until it stabilizes. 32 + continue 33 + } 34 + const prevTextAfterUri = prevMatch.rt.unicodeText.slice( 35 + prevMatch.facet.index.byteEnd, 36 + ) 37 + const nextTextAfterUri = nextMatch.rt.unicodeText.slice( 38 + nextMatch.facet.index.byteEnd, 39 + ) 40 + if (prevTextAfterUri === nextTextAfterUri) { 41 + // The text you're editing is before the link, e.g. 42 + // "abc google.com" -> "abcd google.com". 43 + // This is a good time to add the link. 44 + suggestedUris.add(uri) 45 + continue 46 + } 47 + if (/^\s/m.test(nextTextAfterUri)) { 48 + // The link is followed by a space, e.g. 49 + // "google.com" -> "google.com " or 50 + // "google.com." -> "google.com ". 51 + // This is a clear indicator we can linkify it. 52 + suggestedUris.add(uri) 53 + continue 54 + } 55 + if ( 56 + /^[)]?[.,:;!?)](\s|$)/m.test(prevTextAfterUri) && 57 + /^[)]?[.,:;!?)]\s/m.test(nextTextAfterUri) 58 + ) { 59 + // The link was *already* being followed by punctuation, 60 + // and now it's followed both by punctuation and a space. 61 + // This means you're typing after punctuation, e.g. 62 + // "google.com." -> "google.com. " or 63 + // "google.com.foo" -> "google.com. foo". 64 + // This means you're not typing the link anymore, so we can linkify it. 65 + suggestedUris.add(uri) 66 + continue 67 + } 68 + } 69 + for (const uri of pastSuggestedUris) { 70 + if (!nextDetectedUris.has(uri)) { 71 + // If a link is no longer detected, it's eligible for suggestions next time. 72 + pastSuggestedUris.delete(uri) 73 + } 74 + } 28 75 29 - // We use the isValidUrl regex below because we don't want to add embeds only if the url is valid, i.e. 30 - // http://facebook is a valid url, but that doesn't mean we want to embed it. We should only embed if 31 - // the url is a valid url _and_ domain. new URL() won't work for this check. 32 - const shouldCheck = 33 - cursorLocationIsOkay && !prevAddedLinks.has(uri) && isValidUrlAndDomain(uri) 34 - 35 - if (shouldCheck) { 36 - onNewLink(uri) 37 - prevAddedLinks.add(uri) 76 + let suggestedUri: string | undefined 77 + if (suggestedUris.size > 0) { 78 + suggestedUri = Array.from(suggestedUris)[0] 79 + pastSuggestedUris.add(suggestedUri) 38 80 } 81 + 82 + return suggestedUri 39 83 } 40 84 41 85 // https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url ··· 46 90 value, 47 91 ) 48 92 } 49 - 50 - export function findIndexInText(term: string, text: string) { 51 - // This should find patterns like: 52 - // HELLO SENTENCE http://google.com/ HELLO 53 - // HELLO SENTENCE http://google.com HELLO 54 - // http://google.com/ HELLO. 55 - // http://google.com/. 56 - const pattern = new RegExp(`\\b(${term})(?![/w])`, 'i') 57 - const match = pattern.exec(text) 58 - return match ? match.index : -1 59 - }