Automatically add a link card for URLs in the composer (#3566)
* automatically add a link card for urls in the composer simplify was paste check use a set simplify the cross platform reuse web implementation remove log pasting in the middle of a block of text proper regex dont re-add immediately after paste and remove don't use `byteIndex` lfg automatically add link card * `mayBePaste` * remove accidentally pasted url from commentzio/stable
parent
71c427cea8
commit
046e11de31
|
@ -42,7 +42,6 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {insertMentionAt} from 'lib/strings/mention-manip'
|
import {insertMentionAt} from 'lib/strings/mention-manip'
|
||||||
import {shortenLinks} from 'lib/strings/rich-text-manip'
|
import {shortenLinks} from 'lib/strings/rich-text-manip'
|
||||||
import {toShortUrl} from 'lib/strings/url-helpers'
|
|
||||||
import {colors, gradients, s} from 'lib/styles'
|
import {colors, gradients, s} from 'lib/styles'
|
||||||
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
|
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
|
||||||
import {useDialogStateControlContext} from 'state/dialogs'
|
import {useDialogStateControlContext} from 'state/dialogs'
|
||||||
|
@ -119,7 +118,6 @@ export const ComposePost = observer(function ComposePost({
|
||||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||||
const [labels, setLabels] = useState<string[]>([])
|
const [labels, setLabels] = useState<string[]>([])
|
||||||
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
|
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
|
||||||
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
|
|
||||||
const gallery = useMemo(
|
const gallery = useMemo(
|
||||||
() => new GalleryModel(initImageUris),
|
() => new GalleryModel(initImageUris),
|
||||||
[initImageUris],
|
[initImageUris],
|
||||||
|
@ -189,11 +187,12 @@ export const ComposePost = observer(function ComposePost({
|
||||||
}
|
}
|
||||||
}, [onEscape, isModalActive])
|
}, [onEscape, isModalActive])
|
||||||
|
|
||||||
const onPressAddLinkCard = useCallback(
|
const onNewLink = useCallback(
|
||||||
(uri: string) => {
|
(uri: string) => {
|
||||||
|
if (extLink != null) return
|
||||||
setExtLink({uri, isLoading: true})
|
setExtLink({uri, isLoading: true})
|
||||||
},
|
},
|
||||||
[setExtLink],
|
[extLink, setExtLink],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPhotoPasted = useCallback(
|
const onPhotoPasted = useCallback(
|
||||||
|
@ -430,12 +429,11 @@ export const ComposePost = observer(function ComposePost({
|
||||||
ref={textInput}
|
ref={textInput}
|
||||||
richtext={richtext}
|
richtext={richtext}
|
||||||
placeholder={selectTextInputPlaceholder}
|
placeholder={selectTextInputPlaceholder}
|
||||||
suggestedLinks={suggestedLinks}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
setRichText={setRichText}
|
setRichText={setRichText}
|
||||||
onPhotoPasted={onPhotoPasted}
|
onPhotoPasted={onPhotoPasted}
|
||||||
onPressPublish={onPressPublish}
|
onPressPublish={onPressPublish}
|
||||||
onSuggestedLinksChanged={setSuggestedLinks}
|
onNewLink={onNewLink}
|
||||||
onError={setError}
|
onError={setError}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel={_(msg`Write post`)}
|
accessibilityLabel={_(msg`Write post`)}
|
||||||
|
@ -458,29 +456,6 @@ export const ComposePost = observer(function ComposePost({
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{!extLink && suggestedLinks.size > 0 ? (
|
|
||||||
<View style={s.mb5}>
|
|
||||||
{Array.from(suggestedLinks)
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(url => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={`suggested-${url}`}
|
|
||||||
testID="addLinkCardBtn"
|
|
||||||
style={[pal.borderDark, styles.addExtLinkBtn]}
|
|
||||||
onPress={() => onPressAddLinkCard(url)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Add link card`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Creates a card with a thumbnail. The card links to ${url}`,
|
|
||||||
)}>
|
|
||||||
<Text style={pal.text}>
|
|
||||||
<Trans>Add link card:</Trans>{' '}
|
|
||||||
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
<SuggestedLanguage text={richtext.text} />
|
<SuggestedLanguage text={richtext.text} />
|
||||||
<View style={[pal.border, styles.bottomBar]}>
|
<View style={[pal.border, styles.bottomBar]}>
|
||||||
{canSelectImages ? (
|
{canSelectImages ? (
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, {
|
import React, {
|
||||||
|
ComponentProps,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useRef,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
ComponentProps,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
|
@ -13,22 +13,26 @@ import {
|
||||||
TextInputSelectionChangeEventData,
|
TextInputSelectionChangeEventData,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||||
import PasteInput, {
|
import PasteInput, {
|
||||||
PastedFile,
|
PastedFile,
|
||||||
PasteInputRef,
|
PasteInputRef,
|
||||||
} from '@mattermost/react-native-paste-input'
|
} from '@mattermost/react-native-paste-input'
|
||||||
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
|
||||||
import isEqual from 'lodash.isequal'
|
import {POST_IMG_MAX} from 'lib/constants'
|
||||||
import {Autocomplete} from './mobile/Autocomplete'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {downloadAndResize} from 'lib/media/manip'
|
||||||
|
import {isUriImage} from 'lib/media/util'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {isUriImage} from 'lib/media/util'
|
|
||||||
import {downloadAndResize} from 'lib/media/manip'
|
|
||||||
import {POST_IMG_MAX} from 'lib/constants'
|
|
||||||
import {isIOS} from 'platform/detection'
|
import {isIOS} from 'platform/detection'
|
||||||
|
import {
|
||||||
|
addLinkCardIfNecessary,
|
||||||
|
findIndexInText,
|
||||||
|
} from 'view/com/composer/text-input/text-input-util'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {Autocomplete} from './mobile/Autocomplete'
|
||||||
|
|
||||||
export interface TextInputRef {
|
export interface TextInputRef {
|
||||||
focus: () => void
|
focus: () => void
|
||||||
|
@ -39,11 +43,10 @@ export interface TextInputRef {
|
||||||
interface TextInputProps extends ComponentProps<typeof RNTextInput> {
|
interface TextInputProps extends ComponentProps<typeof RNTextInput> {
|
||||||
richtext: RichText
|
richtext: RichText
|
||||||
placeholder: string
|
placeholder: string
|
||||||
suggestedLinks: Set<string>
|
|
||||||
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
|
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
|
||||||
onPhotoPasted: (uri: string) => void
|
onPhotoPasted: (uri: string) => void
|
||||||
onPressPublish: (richtext: RichText) => Promise<void>
|
onPressPublish: (richtext: RichText) => Promise<void>
|
||||||
onSuggestedLinksChanged: (uris: Set<string>) => void
|
onNewLink: (uri: string) => void
|
||||||
onError: (err: string) => void
|
onError: (err: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,10 +59,9 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||||
{
|
{
|
||||||
richtext,
|
richtext,
|
||||||
placeholder,
|
placeholder,
|
||||||
suggestedLinks,
|
|
||||||
setRichText,
|
setRichText,
|
||||||
onPhotoPasted,
|
onPhotoPasted,
|
||||||
onSuggestedLinksChanged,
|
onNewLink,
|
||||||
onError,
|
onError,
|
||||||
...props
|
...props
|
||||||
}: TextInputProps,
|
}: TextInputProps,
|
||||||
|
@ -70,6 +72,8 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||||
const textInputSelection = useRef<Selection>({start: 0, end: 0})
|
const textInputSelection = useRef<Selection>({start: 0, end: 0})
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const [autocompletePrefix, setAutocompletePrefix] = useState('')
|
const [autocompletePrefix, setAutocompletePrefix] = useState('')
|
||||||
|
const prevLength = React.useRef(richtext.length)
|
||||||
|
const prevAddedLinks = useRef(new Set<string>())
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
focus: () => textInput.current?.focus(),
|
focus: () => textInput.current?.focus(),
|
||||||
|
@ -92,6 +96,8 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||||
* @see https://github.com/bluesky-social/social-app/issues/929
|
* @see https://github.com/bluesky-social/social-app/issues/929
|
||||||
*/
|
*/
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
const mayBePaste = newText.length > prevLength.current + 1
|
||||||
|
|
||||||
const newRt = new RichText({text: newText})
|
const newRt = new RichText({text: newText})
|
||||||
newRt.detectFacetsWithoutResolution()
|
newRt.detectFacetsWithoutResolution()
|
||||||
setRichText(newRt)
|
setRichText(newRt)
|
||||||
|
@ -106,8 +112,6 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||||
setAutocompletePrefix('')
|
setAutocompletePrefix('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const set: Set<string> = new Set()
|
|
||||||
|
|
||||||
if (newRt.facets) {
|
if (newRt.facets) {
|
||||||
for (const facet of newRt.facets) {
|
for (const facet of newRt.facets) {
|
||||||
for (const feature of facet.features) {
|
for (const feature of facet.features) {
|
||||||
|
@ -126,26 +130,32 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||||
onPhotoPasted(res.path)
|
onPhotoPasted(res.path)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
set.add(feature.uri)
|
const cursorLocation = textInputSelection.current.end
|
||||||
|
|
||||||
|
addLinkCardIfNecessary({
|
||||||
|
uri: feature.uri,
|
||||||
|
newText,
|
||||||
|
cursorLocation,
|
||||||
|
mayBePaste,
|
||||||
|
onNewLink,
|
||||||
|
prevAddedLinks: prevAddedLinks.current,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEqual(set, suggestedLinks)) {
|
for (const uri of prevAddedLinks.current.keys()) {
|
||||||
onSuggestedLinksChanged(set)
|
if (findIndexInText(uri, newText) === -1) {
|
||||||
|
prevAddedLinks.current.delete(uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevLength.current = newText.length
|
||||||
}, 1)
|
}, 1)
|
||||||
},
|
},
|
||||||
[
|
[setRichText, autocompletePrefix, onPhotoPasted, prevAddedLinks, onNewLink],
|
||||||
setRichText,
|
|
||||||
autocompletePrefix,
|
|
||||||
setAutocompletePrefix,
|
|
||||||
suggestedLinks,
|
|
||||||
onSuggestedLinksChanged,
|
|
||||||
onPhotoPasted,
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPaste = useCallback(
|
const onPaste = useCallback(
|
||||||
|
|
|
@ -1,28 +1,32 @@
|
||||||
import React from 'react'
|
import React, {useRef} from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {RichText, AppBskyRichtextFacet} from '@atproto/api'
|
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||||
import EventEmitter from 'eventemitter3'
|
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||||
import {useEditor, EditorContent, JSONContent} from '@tiptap/react'
|
import {Trans} from '@lingui/macro'
|
||||||
import {Document} from '@tiptap/extension-document'
|
import {Document} from '@tiptap/extension-document'
|
||||||
import History from '@tiptap/extension-history'
|
|
||||||
import Hardbreak from '@tiptap/extension-hard-break'
|
import Hardbreak from '@tiptap/extension-hard-break'
|
||||||
|
import History from '@tiptap/extension-history'
|
||||||
import {Mention} from '@tiptap/extension-mention'
|
import {Mention} from '@tiptap/extension-mention'
|
||||||
import {Paragraph} from '@tiptap/extension-paragraph'
|
import {Paragraph} from '@tiptap/extension-paragraph'
|
||||||
import {Placeholder} from '@tiptap/extension-placeholder'
|
import {Placeholder} from '@tiptap/extension-placeholder'
|
||||||
import {Text as TiptapText} from '@tiptap/extension-text'
|
import {Text as TiptapText} from '@tiptap/extension-text'
|
||||||
import isEqual from 'lodash.isequal'
|
|
||||||
import {createSuggestion} from './web/Autocomplete'
|
|
||||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
|
||||||
import {isUriImage, blobToDataUri} from 'lib/media/util'
|
|
||||||
import {Emoji} from './web/EmojiPicker.web'
|
|
||||||
import {LinkDecorator} from './web/LinkDecorator'
|
|
||||||
import {generateJSON} from '@tiptap/html'
|
import {generateJSON} from '@tiptap/html'
|
||||||
import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
|
import {EditorContent, JSONContent, useEditor} from '@tiptap/react'
|
||||||
|
import EventEmitter from 'eventemitter3'
|
||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
|
||||||
|
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||||
|
import {blobToDataUri, isUriImage} from 'lib/media/util'
|
||||||
|
import {
|
||||||
|
addLinkCardIfNecessary,
|
||||||
|
findIndexInText,
|
||||||
|
} from 'view/com/composer/text-input/text-input-util'
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
import {Text} from '../../util/text/Text'
|
import {Text} from '../../util/text/Text'
|
||||||
import {Trans} from '@lingui/macro'
|
import {createSuggestion} from './web/Autocomplete'
|
||||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
import {Emoji} from './web/EmojiPicker.web'
|
||||||
|
import {LinkDecorator} from './web/LinkDecorator'
|
||||||
import {TagDecorator} from './web/TagDecorator'
|
import {TagDecorator} from './web/TagDecorator'
|
||||||
|
|
||||||
export interface TextInputRef {
|
export interface TextInputRef {
|
||||||
|
@ -38,7 +42,7 @@ interface TextInputProps {
|
||||||
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
|
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
|
||||||
onPhotoPasted: (uri: string) => void
|
onPhotoPasted: (uri: string) => void
|
||||||
onPressPublish: (richtext: RichText) => Promise<void>
|
onPressPublish: (richtext: RichText) => Promise<void>
|
||||||
onSuggestedLinksChanged: (uris: Set<string>) => void
|
onNewLink: (uri: string) => void
|
||||||
onError: (err: string) => void
|
onError: (err: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,16 +52,17 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||||
{
|
{
|
||||||
richtext,
|
richtext,
|
||||||
placeholder,
|
placeholder,
|
||||||
suggestedLinks,
|
|
||||||
setRichText,
|
setRichText,
|
||||||
onPhotoPasted,
|
onPhotoPasted,
|
||||||
onPressPublish,
|
onPressPublish,
|
||||||
onSuggestedLinksChanged,
|
onNewLink,
|
||||||
}: // onError, TODO
|
}: // onError, TODO
|
||||||
TextInputProps,
|
TextInputProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const autocomplete = useActorAutocompleteFn()
|
const autocomplete = useActorAutocompleteFn()
|
||||||
|
const prevLength = React.useRef(0)
|
||||||
|
const prevAddedLinks = useRef(new Set<string>())
|
||||||
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
|
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
|
||||||
|
@ -180,26 +185,42 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||||
},
|
},
|
||||||
onUpdate({editor: editorProp}) {
|
onUpdate({editor: editorProp}) {
|
||||||
const json = editorProp.getJSON()
|
const json = editorProp.getJSON()
|
||||||
|
const newText = editorJsonToText(json).trimEnd()
|
||||||
|
const mayBePaste = newText.length > prevLength.current + 1
|
||||||
|
|
||||||
const newRt = new RichText({text: editorJsonToText(json).trimEnd()})
|
const newRt = new RichText({text: newText})
|
||||||
newRt.detectFacetsWithoutResolution()
|
newRt.detectFacetsWithoutResolution()
|
||||||
setRichText(newRt)
|
setRichText(newRt)
|
||||||
|
|
||||||
const set: Set<string> = new Set()
|
|
||||||
|
|
||||||
if (newRt.facets) {
|
if (newRt.facets) {
|
||||||
for (const facet of newRt.facets) {
|
for (const facet of newRt.facets) {
|
||||||
for (const feature of facet.features) {
|
for (const feature of facet.features) {
|
||||||
if (AppBskyRichtextFacet.isLink(feature)) {
|
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||||
set.add(feature.uri)
|
// The TipTap editor shows the position as being one character ahead, as if the start index is 1.
|
||||||
|
// Subtracting 1 from the pos gives us the same behavior as the native impl.
|
||||||
|
let cursorLocation = editor?.state.selection.$anchor.pos ?? 1
|
||||||
|
cursorLocation -= 1
|
||||||
|
|
||||||
|
addLinkCardIfNecessary({
|
||||||
|
uri: feature.uri,
|
||||||
|
newText,
|
||||||
|
cursorLocation,
|
||||||
|
mayBePaste,
|
||||||
|
onNewLink,
|
||||||
|
prevAddedLinks: prevAddedLinks.current,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEqual(set, suggestedLinks)) {
|
for (const uri of prevAddedLinks.current.keys()) {
|
||||||
onSuggestedLinksChanged(set)
|
if (findIndexInText(uri, newText) === -1) {
|
||||||
|
prevAddedLinks.current.delete(uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevLength.current = newText.length
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[modeClass],
|
[modeClass],
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
if (shouldCheck) {
|
||||||
|
onNewLink(uri)
|
||||||
|
prevAddedLinks.add(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
|
||||||
|
// question credit Muhammad Imran Tariq https://stackoverflow.com/users/420613/muhammad-imran-tariq
|
||||||
|
// answer credit Christian David https://stackoverflow.com/users/967956/christian-david
|
||||||
|
function isValidUrlAndDomain(value: string) {
|
||||||
|
return /^(?:(?:(?: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.test(
|
||||||
|
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
|
||||||
|
}
|
Loading…
Reference in New Issue