about summary refs log tree commit diff
path: root/src/view/com/composer/text-input/text-input-util.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/composer/text-input/text-input-util.ts')
-rw-r--r--src/view/com/composer/text-input/text-input-util.ts123
1 files changed, 78 insertions, 45 deletions
diff --git a/src/view/com/composer/text-input/text-input-util.ts b/src/view/com/composer/text-input/text-input-util.ts
index 8119e429c..cbe8ef6af 100644
--- a/src/view/com/composer/text-input/text-input-util.ts
+++ b/src/view/com/composer/text-input/text-input-util.ts
@@ -1,41 +1,85 @@
-export function addLinkCardIfNecessary({
-  uri,
-  newText,
-  cursorLocation,
-  mayBePaste,
-  onNewLink,
-  prevAddedLinks,
-}: {
-  uri: string
-  newText: string
-  cursorLocation: number
-  mayBePaste: boolean
-  onNewLink: (uri: string) => void
-  prevAddedLinks: Set<string>
-}) {
-  // It would be cool if we could just use facet.index.byteEnd, but you know... *upside down smiley*
-  const lastCharacterPosition = findIndexInText(uri, newText) + uri.length
+import {AppBskyRichtextFacet, RichText} from '@atproto/api'
 
-  // If the text being added is not from a paste, then we should only check if the cursor is one
-  // position ahead of the last character. However, if it is a paste we need to check both if it's
-  // the same position _or_ one position ahead. That is because iOS will add a space after a paste if
-  // pasting into the middle of a sentence!
-  const cursorLocationIsOkay =
-    cursorLocation === lastCharacterPosition + 1 || mayBePaste
-
-  // Checking previouslyAddedLinks keeps a card from getting added over and over i.e.
-  // Link card added -> Remove link card -> Press back space -> Press space -> Link card added -> and so on
+export type LinkFacetMatch = {
+  rt: RichText
+  facet: AppBskyRichtextFacet.Main
+}
 
-  // We use the isValidUrl regex below because we don't want to add embeds only if the url is valid, i.e.
-  // http://facebook is a valid url, but that doesn't mean we want to embed it. We should only embed if
-  // the url is a valid url _and_ domain. new URL() won't work for this check.
-  const shouldCheck =
-    cursorLocationIsOkay && !prevAddedLinks.has(uri) && isValidUrlAndDomain(uri)
+export function suggestLinkCardUri(
+  mayBePaste: boolean,
+  nextDetectedUris: Map<string, LinkFacetMatch>,
+  prevDetectedUris: Map<string, LinkFacetMatch>,
+  pastSuggestedUris: Set<string>,
+): string | undefined {
+  const suggestedUris = new Set<string>()
+  for (const [uri, nextMatch] of nextDetectedUris) {
+    if (!isValidUrlAndDomain(uri)) {
+      continue
+    }
+    if (pastSuggestedUris.has(uri)) {
+      // Don't suggest already added or already dismissed link cards.
+      continue
+    }
+    if (mayBePaste) {
+      // Immediately add the pasted link without waiting to type more.
+      suggestedUris.add(uri)
+      continue
+    }
+    const prevMatch = prevDetectedUris.get(uri)
+    if (!prevMatch) {
+      // If the same exact link wasn't already detected during the last keystroke,
+      // it means you're probably still typing it. Disregard until it stabilizes.
+      continue
+    }
+    const prevTextAfterUri = prevMatch.rt.unicodeText.slice(
+      prevMatch.facet.index.byteEnd,
+    )
+    const nextTextAfterUri = nextMatch.rt.unicodeText.slice(
+      nextMatch.facet.index.byteEnd,
+    )
+    if (prevTextAfterUri === nextTextAfterUri) {
+      // The text you're editing is before the link, e.g.
+      // "abc google.com" -> "abcd google.com".
+      // This is a good time to add the link.
+      suggestedUris.add(uri)
+      continue
+    }
+    if (/^\s/m.test(nextTextAfterUri)) {
+      // The link is followed by a space, e.g.
+      // "google.com" -> "google.com " or
+      // "google.com." -> "google.com ".
+      // This is a clear indicator we can linkify it.
+      suggestedUris.add(uri)
+      continue
+    }
+    if (
+      /^[)]?[.,:;!?)](\s|$)/m.test(prevTextAfterUri) &&
+      /^[)]?[.,:;!?)]\s/m.test(nextTextAfterUri)
+    ) {
+      // The link was *already* being followed by punctuation,
+      // and now it's followed both by punctuation and a space.
+      // This means you're typing after punctuation, e.g.
+      // "google.com." -> "google.com. " or
+      // "google.com.foo" -> "google.com. foo".
+      // This means you're not typing the link anymore, so we can linkify it.
+      suggestedUris.add(uri)
+      continue
+    }
+  }
+  for (const uri of pastSuggestedUris) {
+    if (!nextDetectedUris.has(uri)) {
+      // If a link is no longer detected, it's eligible for suggestions next time.
+      pastSuggestedUris.delete(uri)
+    }
+  }
 
-  if (shouldCheck) {
-    onNewLink(uri)
-    prevAddedLinks.add(uri)
+  let suggestedUri: string | undefined
+  if (suggestedUris.size > 0) {
+    suggestedUri = Array.from(suggestedUris)[0]
+    pastSuggestedUris.add(suggestedUri)
   }
+
+  return suggestedUri
 }
 
 // https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
@@ -46,14 +90,3 @@ function isValidUrlAndDomain(value: string) {
     value,
   )
 }
-
-export function findIndexInText(term: string, text: string) {
-  // This should find patterns like:
-  // HELLO SENTENCE http://google.com/ HELLO
-  // HELLO SENTENCE http://google.com HELLO
-  // http://google.com/ HELLO.
-  // http://google.com/.
-  const pattern = new RegExp(`\\b(${term})(?![/w])`, 'i')
-  const match = pattern.exec(text)
-  return match ? match.index : -1
-}