diff options
author | Hailey <me@haileyok.com> | 2024-09-27 15:26:28 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-27 15:26:28 -0700 |
commit | 587c0c625752964d8ce64faf1d329dce3c834a5c (patch) | |
tree | 8a345e754db3536b8d0abf875b67f0c6e200d47d | |
parent | 4b5d6e6efb09a714d82e2093dec39c85400a2de6 (diff) | |
download | voidsky-587c0c625752964d8ce64faf1d329dce3c834a5c.tar.zst |
Rework native autocomplete (#5521)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
-rw-r--r-- | src/lib/custom-animations/PressableScale.tsx | 17 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.tsx | 6 | ||||
-rw-r--r-- | src/view/com/composer/text-input/mobile/Autocomplete.tsx | 195 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBar.tsx | 7 |
4 files changed, 105 insertions, 120 deletions
diff --git a/src/lib/custom-animations/PressableScale.tsx b/src/lib/custom-animations/PressableScale.tsx index d6eabf8b2..ca080dc8a 100644 --- a/src/lib/custom-animations/PressableScale.tsx +++ b/src/lib/custom-animations/PressableScale.tsx @@ -13,17 +13,19 @@ import {isNative} from '#/platform/detection' const DEFAULT_TARGET_SCALE = isNative || isTouchDevice ? 0.98 : 1 +const AnimatedPressable = Animated.createAnimatedComponent(Pressable) + export function PressableScale({ targetScale = DEFAULT_TARGET_SCALE, children, - contentContainerStyle, + style, onPressIn, onPressOut, ...rest }: { targetScale?: number - contentContainerStyle?: StyleProp<ViewStyle> -} & Exclude<PressableProps, 'onPressIn' | 'onPressOut'>) { + style?: StyleProp<ViewStyle> +} & Exclude<PressableProps, 'onPressIn' | 'onPressOut' | 'style'>) { const scale = useSharedValue(1) const animatedStyle = useAnimatedStyle(() => ({ @@ -31,7 +33,7 @@ export function PressableScale({ })) return ( - <Pressable + <AnimatedPressable accessibilityRole="button" onPressIn={e => { 'worklet' @@ -49,10 +51,9 @@ export function PressableScale({ cancelAnimation(scale) scale.value = withTiming(1, {duration: 100}) }} + style={[animatedStyle, style]} {...rest}> - <Animated.View style={[animatedStyle, contentContainerStyle]}> - {children as React.ReactNode} - </Animated.View> - </Pressable> + {children} + </AnimatedPressable> ) } diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 3df9cfca4..39baa2cb6 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -245,7 +245,11 @@ export const TextInput = forwardRef(function TextInputImpl( multiline scrollEnabled={false} numberOfLines={4} - style={[inputTextStyle, a.w_full, {textAlignVertical: 'top'}]} + style={[ + inputTextStyle, + a.w_full, + {textAlignVertical: 'top', minHeight: 60}, + ]} {...props}> {textDecorated} </PasteInput> diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx index 9c8f8f916..3d2bcfa61 100644 --- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx +++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx @@ -1,13 +1,17 @@ -import React, {useEffect, useRef} from 'react' -import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' -import {usePalette} from 'lib/hooks/usePalette' -import {Text} from 'view/com/util/text/Text' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {useGrapheme} from '../hooks/useGrapheme' -import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' -import {Trans} from '@lingui/macro' +import React, {useRef} from 'react' +import {View} from 'react-native' +import Animated, {FadeInDown, FadeOut} from 'react-native-reanimated' import {AppBskyActorDefs} from '@atproto/api' +import {Trans} from '@lingui/macro' + +import {PressableScale} from '#/lib/custom-animations/PressableScale' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import {useGrapheme} from '../hooks/useGrapheme' export function Autocomplete({ prefix, @@ -16,8 +20,8 @@ export function Autocomplete({ prefix: string onSelect: (item: string) => void }) { - const pal = usePalette('default') - const positionInterp = useAnimatedValue(0) + const t = useTheme() + const {getGraphemeString} = useGrapheme() const isActive = !!prefix const {data: suggestions, isFetching} = useActorAutocompleteQuery(prefix) @@ -28,108 +32,85 @@ export function Autocomplete({ suggestionsRef.current = suggestions } - useEffect(() => { - Animated.timing(positionInterp, { - toValue: isActive ? 1 : 0, - duration: 200, - useNativeDriver: true, - }).start() - }, [positionInterp, isActive]) - - const topAnimStyle = { - transform: [ - { - translateY: positionInterp.interpolate({ - inputRange: [0, 1], - outputRange: [200, 0], - }), - }, - ], - } + if (!isActive) return null return ( - <Animated.View style={topAnimStyle}> - {isActive ? ( - <View style={[pal.view, styles.container, pal.border]}> - {suggestionsRef.current?.length ? ( - suggestionsRef.current.slice(0, 5).map(item => { - // Eventually use an average length - const MAX_CHARS = 40 - const MAX_HANDLE_CHARS = 20 + <Animated.View + entering={FadeInDown.duration(200)} + exiting={FadeOut.duration(100)} + style={[ + t.atoms.bg, + a.mt_sm, + a.border, + a.rounded_sm, + t.atoms.border_contrast_high, + {marginLeft: -62}, + ]}> + {suggestionsRef.current?.length ? ( + suggestionsRef.current.slice(0, 5).map((item, index, arr) => { + // Eventually use an average length + const MAX_CHARS = 40 + const MAX_HANDLE_CHARS = 20 - // Using this approach because styling is not respecting - // bounding box wrapping (before converting to ellipsis) - const {name: displayHandle, remainingCharacters} = - getGraphemeString(item.handle, MAX_HANDLE_CHARS) + // Using this approach because styling is not respecting + // bounding box wrapping (before converting to ellipsis) + const {name: displayHandle, remainingCharacters} = getGraphemeString( + item.handle, + MAX_HANDLE_CHARS, + ) - const {name: displayName} = getGraphemeString( - item.displayName ?? item.handle, - MAX_CHARS - - MAX_HANDLE_CHARS + - (remainingCharacters > 0 ? remainingCharacters : 0), - ) + const {name: displayName} = getGraphemeString( + item.displayName || item.handle, + MAX_CHARS - + MAX_HANDLE_CHARS + + (remainingCharacters > 0 ? remainingCharacters : 0), + ) - return ( - <TouchableOpacity - testID="autocompleteButton" - key={item.handle} - style={[pal.border, styles.item]} - onPress={() => onSelect(item.handle)} - accessibilityLabel={`Select ${item.handle}`} - accessibilityHint=""> - <View style={styles.avatarAndHandle}> - <UserAvatar - avatar={item.avatar ?? null} - size={24} - type={item.associated?.labeler ? 'labeler' : 'user'} - /> - <Text type="md-medium" style={pal.text}> - {displayName} - </Text> - </View> - <Text type="sm" style={pal.textLight} numberOfLines={1}> - @{displayHandle} + return ( + <View + style={[ + index !== arr.length - 1 && a.border_b, + t.atoms.border_contrast_high, + a.px_sm, + a.py_md, + ]} + key={item.handle}> + <PressableScale + testID="autocompleteButton" + style={[ + a.flex_row, + a.gap_sm, + a.justify_between, + a.align_center, + ]} + onPress={() => onSelect(item.handle)} + accessibilityLabel={`Select ${item.handle}`} + accessibilityHint=""> + <View style={[a.flex_row, a.gap_sm, a.align_center]}> + <UserAvatar + avatar={item.avatar ?? null} + size={24} + type={item.associated?.labeler ? 'labeler' : 'user'} + /> + <Text + style={[a.text_md, a.font_bold]} + emoji={true} + numberOfLines={1}> + {sanitizeDisplayName(displayName)} </Text> - </TouchableOpacity> - ) - }) - ) : ( - <Text type="sm" style={[pal.text, pal.border, styles.noResults]}> - {isFetching ? ( - <Trans>Loading...</Trans> - ) : ( - <Trans>No result</Trans> - )} - </Text> - )} - </View> - ) : null} + </View> + <Text style={[t.atoms.text_contrast_medium]} numberOfLines={1}> + {sanitizeHandle(displayHandle, '@')} + </Text> + </PressableScale> + </View> + ) + }) + ) : ( + <Text style={[a.text_md, a.px_sm, a.py_md]}> + {isFetching ? <Trans>Loading...</Trans> : <Trans>No result</Trans>} + </Text> + )} </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, - }, - avatarAndHandle: { - display: 'flex', - flexDirection: 'row', - gap: 6, - alignItems: 'center', - }, - noResults: { - paddingVertical: 12, - }, -}) diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 9187b5321..af06134fc 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -351,17 +351,16 @@ function Btn({ return ( <PressableScale testID={testID} - style={styles.ctrl} + style={[styles.ctrl, a.flex_1]} onPress={onPress} onLongPress={onLongPress} accessible={accessible} accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} - targetScale={0.8} - contentContainerStyle={[a.flex_1]}> + targetScale={0.8}> {icon} {notificationCount ? ( - <View style={[styles.notificationCount, {top: -5}]}> + <View style={[styles.notificationCount]}> <Text style={styles.notificationCountLabel}>{notificationCount}</Text> </View> ) : undefined} |