···11-export function addLinkCardIfNecessary({
22- uri,
33- newText,
44- cursorLocation,
55- mayBePaste,
66- onNewLink,
77- prevAddedLinks,
88-}: {
99- uri: string
1010- newText: string
1111- cursorLocation: number
1212- mayBePaste: boolean
1313- onNewLink: (uri: string) => void
1414- prevAddedLinks: Set<string>
1515-}) {
1616- // It would be cool if we could just use facet.index.byteEnd, but you know... *upside down smiley*
1717- const lastCharacterPosition = findIndexInText(uri, newText) + uri.length
11+import {AppBskyRichtextFacet, RichText} from '@atproto/api'
1821919- // If the text being added is not from a paste, then we should only check if the cursor is one
2020- // position ahead of the last character. However, if it is a paste we need to check both if it's
2121- // the same position _or_ one position ahead. That is because iOS will add a space after a paste if
2222- // pasting into the middle of a sentence!
2323- const cursorLocationIsOkay =
2424- cursorLocation === lastCharacterPosition + 1 || mayBePaste
33+export type LinkFacetMatch = {
44+ rt: RichText
55+ facet: AppBskyRichtextFacet.Main
66+}
2572626- // Checking previouslyAddedLinks keeps a card from getting added over and over i.e.
2727- // Link card added -> Remove link card -> Press back space -> Press space -> Link card added -> and so on
88+export function suggestLinkCardUri(
99+ mayBePaste: boolean,
1010+ nextDetectedUris: Map<string, LinkFacetMatch>,
1111+ prevDetectedUris: Map<string, LinkFacetMatch>,
1212+ pastSuggestedUris: Set<string>,
1313+): string | undefined {
1414+ const suggestedUris = new Set<string>()
1515+ for (const [uri, nextMatch] of nextDetectedUris) {
1616+ if (!isValidUrlAndDomain(uri)) {
1717+ continue
1818+ }
1919+ if (pastSuggestedUris.has(uri)) {
2020+ // Don't suggest already added or already dismissed link cards.
2121+ continue
2222+ }
2323+ if (mayBePaste) {
2424+ // Immediately add the pasted link without waiting to type more.
2525+ suggestedUris.add(uri)
2626+ continue
2727+ }
2828+ const prevMatch = prevDetectedUris.get(uri)
2929+ if (!prevMatch) {
3030+ // If the same exact link wasn't already detected during the last keystroke,
3131+ // it means you're probably still typing it. Disregard until it stabilizes.
3232+ continue
3333+ }
3434+ const prevTextAfterUri = prevMatch.rt.unicodeText.slice(
3535+ prevMatch.facet.index.byteEnd,
3636+ )
3737+ const nextTextAfterUri = nextMatch.rt.unicodeText.slice(
3838+ nextMatch.facet.index.byteEnd,
3939+ )
4040+ if (prevTextAfterUri === nextTextAfterUri) {
4141+ // The text you're editing is before the link, e.g.
4242+ // "abc google.com" -> "abcd google.com".
4343+ // This is a good time to add the link.
4444+ suggestedUris.add(uri)
4545+ continue
4646+ }
4747+ if (/^\s/m.test(nextTextAfterUri)) {
4848+ // The link is followed by a space, e.g.
4949+ // "google.com" -> "google.com " or
5050+ // "google.com." -> "google.com ".
5151+ // This is a clear indicator we can linkify it.
5252+ suggestedUris.add(uri)
5353+ continue
5454+ }
5555+ if (
5656+ /^[)]?[.,:;!?)](\s|$)/m.test(prevTextAfterUri) &&
5757+ /^[)]?[.,:;!?)]\s/m.test(nextTextAfterUri)
5858+ ) {
5959+ // The link was *already* being followed by punctuation,
6060+ // and now it's followed both by punctuation and a space.
6161+ // This means you're typing after punctuation, e.g.
6262+ // "google.com." -> "google.com. " or
6363+ // "google.com.foo" -> "google.com. foo".
6464+ // This means you're not typing the link anymore, so we can linkify it.
6565+ suggestedUris.add(uri)
6666+ continue
6767+ }
6868+ }
6969+ for (const uri of pastSuggestedUris) {
7070+ if (!nextDetectedUris.has(uri)) {
7171+ // If a link is no longer detected, it's eligible for suggestions next time.
7272+ pastSuggestedUris.delete(uri)
7373+ }
7474+ }
28752929- // We use the isValidUrl regex below because we don't want to add embeds only if the url is valid, i.e.
3030- // http://facebook is a valid url, but that doesn't mean we want to embed it. We should only embed if
3131- // the url is a valid url _and_ domain. new URL() won't work for this check.
3232- const shouldCheck =
3333- cursorLocationIsOkay && !prevAddedLinks.has(uri) && isValidUrlAndDomain(uri)
3434-3535- if (shouldCheck) {
3636- onNewLink(uri)
3737- prevAddedLinks.add(uri)
7676+ let suggestedUri: string | undefined
7777+ if (suggestedUris.size > 0) {
7878+ suggestedUri = Array.from(suggestedUris)[0]
7979+ pastSuggestedUris.add(suggestedUri)
3880 }
8181+8282+ return suggestedUri
3983}
40844185// https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
···4690 value,
4791 )
4892}
4949-5050-export function findIndexInText(term: string, text: string) {
5151- // This should find patterns like:
5252- // HELLO SENTENCE http://google.com/ HELLO
5353- // HELLO SENTENCE http://google.com HELLO
5454- // http://google.com/ HELLO.
5555- // http://google.com/.
5656- const pattern = new RegExp(`\\b(${term})(?![/w])`, 'i')
5757- const match = pattern.exec(text)
5858- return match ? match.index : -1
5959-}