diff options
-rw-r--r-- | src/view/com/modals/ComposePost.tsx | 62 | ||||
-rw-r--r-- | src/view/com/modals/composer/Autocomplete.tsx | 76 | ||||
-rw-r--r-- | todos.txt | 1 |
3 files changed, 130 insertions, 9 deletions
diff --git a/src/view/com/modals/ComposePost.tsx b/src/view/com/modals/ComposePost.tsx index 22b6b14bb..b55143098 100644 --- a/src/view/com/modals/ComposePost.tsx +++ b/src/view/com/modals/ComposePost.tsx @@ -1,8 +1,9 @@ -import React, {useState} from 'react' +import React, {useMemo, useState} from 'react' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {BottomSheetTextInput} from '@gorhom/bottom-sheet' import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Autocomplete} from './composer/Autocomplete' import Toast from '../util/Toast' import ProgressCircle from '../util/ProgressCircle' import {useStores} from '../../../state' @@ -14,16 +15,27 @@ const WARNING_TEXT_LENGTH = 200 const DANGER_TEXT_LENGTH = 255 export const snapPoints = ['100%'] +const DEBUG_USERNAMES = ['alice.com', 'bob.com', 'carla.com'] + export function Component({replyTo}: {replyTo?: string}) { const store = useStores() - const [text, setText] = useState('') const [error, setError] = useState('') + const [text, setText] = useState('') + const [autocompleteOptions, setAutocompleteOptions] = useState<string[]>([]) const onChangeText = (newText: string) => { if (newText.length > MAX_TEXT_LENGTH) { - setText(newText.slice(0, MAX_TEXT_LENGTH)) - } else { - setText(newText) + newText = newText.slice(0, MAX_TEXT_LENGTH) + } + setText(newText) + + const prefix = extractTextAutocompletePrefix(newText) + if (typeof prefix === 'string') { + setAutocompleteOptions( + DEBUG_USERNAMES.filter(name => name.includes(prefix)), + ) + } else if (autocompleteOptions) { + setAutocompleteOptions([]) } } const onPressCancel = () => { @@ -53,6 +65,10 @@ export function Component({replyTo}: {replyTo?: string}) { hideOnPress: true, }) } + const onSelectAutocompleteItem = (item: string) => { + setText(replaceTextAutocompletePrefix(text, item)) + setAutocompleteOptions([]) + } const progressColor = text.length > DANGER_TEXT_LENGTH @@ -61,6 +77,19 @@ export function Component({replyTo}: {replyTo?: string}) { ? '#f7c600' : 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 ( <View style={styles.outer}> <View style={styles.topbar}> @@ -95,10 +124,10 @@ export function Component({replyTo}: {replyTo?: string}) { scrollEnabled autoFocus onChangeText={(text: string) => onChangeText(text)} - value={text} 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.flex1} /> <View> @@ -108,10 +137,27 @@ export function Component({replyTo}: {replyTo?: string}) { /> </View> </View> + <Autocomplete + active={autocompleteOptions.length > 0} + items={autocompleteOptions} + onSelect={onSelectAutocompleteItem} + /> </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({ outer: { flexDirection: 'column', diff --git a/src/view/com/modals/composer/Autocomplete.tsx b/src/view/com/modals/composer/Autocomplete.tsx new file mode 100644 index 000000000..4e4bdfc8e --- /dev/null +++ b/src/view/com/modals/composer/Autocomplete.tsx @@ -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, + }, +}) diff --git a/todos.txt b/todos.txt index 91ccb43f6..965af5c1d 100644 --- a/todos.txt +++ b/todos.txt @@ -2,7 +2,6 @@ Paul's todo list - General - Update to RN 0.70 - - Selector swipe gesture - Composer - Update the view after creating a post - Profile |