Implement autocomplete UI in composer

zio/stable
Paul Frazee 2022-09-08 13:39:53 -05:00
parent 9010078489
commit 35556a84b2
3 changed files with 130 additions and 9 deletions

View File

@ -1,8 +1,9 @@
import React, {useState} from 'react' import React, {useMemo, useState} from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {BottomSheetTextInput} from '@gorhom/bottom-sheet' import {BottomSheetTextInput} from '@gorhom/bottom-sheet'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Autocomplete} from './composer/Autocomplete'
import Toast from '../util/Toast' import Toast from '../util/Toast'
import ProgressCircle from '../util/ProgressCircle' import ProgressCircle from '../util/ProgressCircle'
import {useStores} from '../../../state' import {useStores} from '../../../state'
@ -14,16 +15,27 @@ const WARNING_TEXT_LENGTH = 200
const DANGER_TEXT_LENGTH = 255 const DANGER_TEXT_LENGTH = 255
export const snapPoints = ['100%'] export const snapPoints = ['100%']
const DEBUG_USERNAMES = ['alice.com', 'bob.com', 'carla.com']
export function Component({replyTo}: {replyTo?: string}) { export function Component({replyTo}: {replyTo?: string}) {
const store = useStores() const store = useStores()
const [text, setText] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [text, setText] = useState('')
const [autocompleteOptions, setAutocompleteOptions] = useState<string[]>([])
const onChangeText = (newText: string) => { const onChangeText = (newText: string) => {
if (newText.length > MAX_TEXT_LENGTH) { if (newText.length > MAX_TEXT_LENGTH) {
setText(newText.slice(0, MAX_TEXT_LENGTH)) newText = newText.slice(0, MAX_TEXT_LENGTH)
} else { }
setText(newText) setText(newText)
const prefix = extractTextAutocompletePrefix(newText)
if (typeof prefix === 'string') {
setAutocompleteOptions(
DEBUG_USERNAMES.filter(name => name.includes(prefix)),
)
} else if (autocompleteOptions) {
setAutocompleteOptions([])
} }
} }
const onPressCancel = () => { const onPressCancel = () => {
@ -53,6 +65,10 @@ export function Component({replyTo}: {replyTo?: string}) {
hideOnPress: true, hideOnPress: true,
}) })
} }
const onSelectAutocompleteItem = (item: string) => {
setText(replaceTextAutocompletePrefix(text, item))
setAutocompleteOptions([])
}
const progressColor = const progressColor =
text.length > DANGER_TEXT_LENGTH text.length > DANGER_TEXT_LENGTH
@ -61,6 +77,19 @@ export function Component({replyTo}: {replyTo?: string}) {
? '#f7c600' ? '#f7c600'
: undefined : undefined
const textDecorated = useMemo(() => {
return (text || '').split(/(\s)/g).map((item, i) => {
if (/@[a-zA-Z0-9]+/g.test(item)) {
return (
<Text key={i} style={{color: colors.blue3}}>
{item}
</Text>
)
}
return item
})
}, [text])
return ( return (
<View style={styles.outer}> <View style={styles.outer}>
<View style={styles.topbar}> <View style={styles.topbar}>
@ -95,10 +124,10 @@ export function Component({replyTo}: {replyTo?: string}) {
scrollEnabled scrollEnabled
autoFocus autoFocus
onChangeText={(text: string) => onChangeText(text)} onChangeText={(text: string) => onChangeText(text)}
value={text}
placeholder={replyTo ? 'Write your reply' : "What's new?"} placeholder={replyTo ? 'Write your reply' : "What's new?"}
style={styles.textInput} style={styles.textInput}>
/> {textDecorated}
</BottomSheetTextInput>
<View style={[s.flexRow, s.pt10, s.pb10, s.pr5]}> <View style={[s.flexRow, s.pt10, s.pb10, s.pr5]}>
<View style={s.flex1} /> <View style={s.flex1} />
<View> <View>
@ -108,10 +137,27 @@ export function Component({replyTo}: {replyTo?: string}) {
/> />
</View> </View>
</View> </View>
<Autocomplete
active={autocompleteOptions.length > 0}
items={autocompleteOptions}
onSelect={onSelectAutocompleteItem}
/>
</View> </View>
) )
} }
const atPrefixRegex = /@([\S]*)$/i
function extractTextAutocompletePrefix(text: string) {
const match = atPrefixRegex.exec(text)
if (match) {
return match[1]
}
return undefined
}
function replaceTextAutocompletePrefix(text: string, item: string) {
return text.replace(atPrefixRegex, `@${item} `)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
flexDirection: 'column', flexDirection: 'column',

View File

@ -0,0 +1,76 @@
import React, {useEffect} from 'react'
import {
useWindowDimensions,
Text,
TouchableOpacity,
StyleSheet,
} from 'react-native'
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
interpolate,
} from 'react-native-reanimated'
import {colors} from '../../../lib/styles'
export function Autocomplete({
active,
items,
onSelect,
}: {
active: boolean
items: string[]
onSelect: (item: string) => void
}) {
const winDim = useWindowDimensions()
const positionInterp = useSharedValue<number>(0)
useEffect(() => {
if (active) {
positionInterp.value = withTiming(1, {duration: 250})
} else {
positionInterp.value = withTiming(0, {duration: 250})
}
}, [positionInterp, active])
const topAnimStyle = useAnimatedStyle(() => ({
top: interpolate(
positionInterp.value,
[0, 1.0],
[winDim.height, winDim.height / 4],
),
}))
return (
<Animated.View style={[styles.outer, topAnimStyle]}>
{items.map((item, i) => (
<TouchableOpacity
key={i}
style={styles.item}
onPress={() => onSelect(item)}>
<Text style={styles.itemText}>@{item}</Text>
</TouchableOpacity>
))}
</Animated.View>
)
}
const styles = StyleSheet.create({
outer: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.gray2,
},
item: {
borderBottomWidth: 1,
borderBottomColor: colors.gray1,
paddingVertical: 16,
paddingHorizontal: 16,
},
itemText: {
fontSize: 16,
},
})

View File

@ -2,7 +2,6 @@ Paul's todo list
- General - General
- Update to RN 0.70 - Update to RN 0.70
- Selector swipe gesture
- Composer - Composer
- Update the view after creating a post - Update the view after creating a post
- Profile - Profile