mobile autocomplete

This commit is contained in:
Eric Bailey
2023-09-27 21:22:47 -05:00
parent 4d367d6805
commit 564a654cc2
3 changed files with 161 additions and 4 deletions
+9 -1
View File
@@ -13,7 +13,7 @@ import {
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {RichText} from '@atproto/api'
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import {useAnalytics} from 'lib/analytics/analytics'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete'
@@ -226,6 +226,14 @@ export const ComposePost = observer(function ComposePost({
imageCount: gallery.size,
})
if (replyTo && replyTo.uri) track('Post:Reply')
for (const facet of richtext.facets || []) {
for (const feature of facet.features) {
if (AppBskyRichtextFacet.isTag(feature)) {
tagsAutocompleteModel.commitRecentTag(feature.tag)
}
}
}
}
if (!replyTo) {
store.me.mainFeed.onPostCreated()
+34 -3
View File
@@ -21,6 +21,11 @@ import isEqual from 'lodash.isequal'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete'
import {Autocomplete} from './mobile/Autocomplete'
import {
TagsAutocomplete,
getHashtagAt,
insertTagAt,
} from './mobile/TagsAutocomplete'
import {Text} from 'view/com/util/text/Text'
import {cleanError} from 'lib/strings/errors'
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
@@ -59,6 +64,7 @@ export const TextInput = forwardRef(function TextInputImpl(
placeholder,
suggestedLinks,
autocompleteView,
tagsAutocompleteModel,
setRichText,
onPhotoPasted,
onSuggestedLinksChanged,
@@ -96,17 +102,29 @@ export const TextInput = forwardRef(function TextInputImpl(
newRt.detectFacetsWithoutResolution()
setRichText(newRt)
const prefix = getMentionAt(
const mentionPrefix = getMentionAt(
newText,
textInputSelection.current?.start || 0,
)
if (prefix) {
if (mentionPrefix) {
autocompleteView.setActive(true)
autocompleteView.setPrefix(prefix.value)
autocompleteView.setPrefix(mentionPrefix.value)
} else {
autocompleteView.setActive(false)
}
const hashtagPrefix = getHashtagAt(
newText,
textInputSelection.current?.start || 0,
)
if (hashtagPrefix) {
tagsAutocompleteModel.setActive(true)
tagsAutocompleteModel.search(hashtagPrefix.value || '')
} else {
tagsAutocompleteModel.setActive(false)
}
const set: Set<string> = new Set()
if (newRt.facets) {
@@ -145,6 +163,7 @@ export const TextInput = forwardRef(function TextInputImpl(
suggestedLinks,
onSuggestedLinksChanged,
onPhotoPasted,
tagsAutocompleteModel,
],
)
@@ -186,6 +205,17 @@ export const TextInput = forwardRef(function TextInputImpl(
[onChangeText, richtext, autocompleteView],
)
const onSelectTag = useCallback(
(tag: string) => {
onChangeText(
insertTagAt(richtext.text, textInputSelection.current?.start || 0, tag),
)
tagsAutocompleteModel.commitRecentTag(tag)
tagsAutocompleteModel.setActive(false)
},
[onChangeText, richtext, tagsAutocompleteModel],
)
const textDecorated = useMemo(() => {
let i = 0
@@ -223,6 +253,7 @@ export const TextInput = forwardRef(function TextInputImpl(
view={autocompleteView}
onSelect={onSelectAutocompleteItem}
/>
<TagsAutocomplete model={tagsAutocompleteModel} onSelect={onSelectTag} />
</View>
)
})
@@ -0,0 +1,118 @@
import React, {useEffect} from 'react'
import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette'
import {Text} from 'view/com/util/text/Text'
export function getHashtagAt(text: string, position: number) {
const regex = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/gi
let match
while ((match = regex.exec(text))) {
const [matchedString, tag] = match
if (tag.length > 66) continue
const from = match.index + matchedString.indexOf(tag)
const to = from + tag.length
if (position >= from && position <= to) {
return {value: tag, index: from}
}
}
const hashRegex = /#/g
let hashMatch
while ((hashMatch = hashRegex.exec(text))) {
if (position >= hashMatch.index && position <= hashMatch.index + 1) {
return {value: '', index: hashMatch.index}
}
}
return undefined
}
export function insertTagAt(text: string, position: number, tag: string) {
const target = getHashtagAt(text, position)
if (target) {
return `${text.slice(0, target.index)}#${tag} ${text.slice(
target.index + target.value.length + 1, // add 1 to include the "@"
)}`
}
return text
}
export const TagsAutocomplete = observer(function AutocompleteImpl({
model,
onSelect,
}: {
model: TagsAutocompleteModel
onSelect: (item: string) => void
}) {
const pal = usePalette('default')
const positionInterp = useAnimatedValue(0)
useEffect(() => {
Animated.timing(positionInterp, {
toValue: model.isActive ? 1 : 0,
duration: 200,
useNativeDriver: true,
}).start()
}, [positionInterp, model.isActive])
const topAnimStyle = {
transform: [
{
translateY: positionInterp.interpolate({
inputRange: [0, 1],
outputRange: [200, 0],
}),
},
],
}
if (!model.suggestions.length) return null
return (
<Animated.View style={topAnimStyle}>
{model.isActive ? (
<View style={[pal.view, styles.container, pal.border]}>
{model.suggestions.slice(0, 5).map(item => {
return (
<TouchableOpacity
testID="autocompleteButton"
key={item}
style={[pal.border, styles.item]}
onPress={() => onSelect(item)}
accessibilityLabel={`Select #${item}`}
accessibilityHint="">
<Text type="sm" style={pal.textLight} numberOfLines={1}>
#{item}
</Text>
</TouchableOpacity>
)
})}
</View>
) : null}
</Animated.View>
)
})
const styles = StyleSheet.create({
container: {
marginLeft: -50, // Composer avatar width
top: 10,
borderTopWidth: 1,
},
item: {
borderBottomWidth: 1,
paddingVertical: 12,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 6,
},
})