From 587c0c625752964d8ce64faf1d329dce3c834a5c Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 27 Sep 2024 15:26:28 -0700 Subject: [PATCH] Rework native autocomplete (#5521) Co-authored-by: Samuel Newman --- src/lib/custom-animations/PressableScale.tsx | 17 +- .../com/composer/text-input/TextInput.tsx | 6 +- .../text-input/mobile/Autocomplete.tsx | 195 ++++++++---------- 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 d6eabf8b22..ca080dc8ae 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 -} & Exclude) { + style?: StyleProp +} & Exclude) { const scale = useSharedValue(1) const animatedStyle = useAnimatedStyle(() => ({ @@ -31,7 +33,7 @@ export function PressableScale({ })) return ( - { 'worklet' @@ -49,10 +51,9 @@ export function PressableScale({ cancelAnimation(scale) scale.value = withTiming(1, {duration: 100}) }} + style={[animatedStyle, style]} {...rest}> - - {children as React.ReactNode} - - + {children} + ) } diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 3df9cfca47..39baa2cb65 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} diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx index 9c8f8f9162..3d2bcfa61e 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 ( - - {isActive ? ( - - {suggestionsRef.current?.length ? ( - suggestionsRef.current.slice(0, 5).map(item => { - // Eventually use an average length - const MAX_CHARS = 40 - const MAX_HANDLE_CHARS = 20 + + {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 ( - onSelect(item.handle)} - accessibilityLabel={`Select ${item.handle}`} - accessibilityHint=""> - - - - {displayName} - - - - @{displayHandle} + return ( + + onSelect(item.handle)} + accessibilityLabel={`Select ${item.handle}`} + accessibilityHint=""> + + + + {sanitizeDisplayName(displayName)} - - ) - }) - ) : ( - - {isFetching ? ( - Loading... - ) : ( - No result - )} - - )} - - ) : null} + + + {sanitizeHandle(displayHandle, '@')} + + + + ) + }) + ) : ( + + {isFetching ? Loading... : No result} + + )} ) } - -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 9187b53218..af06134fce 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -351,17 +351,16 @@ function Btn({ return ( + targetScale={0.8}> {icon} {notificationCount ? ( - + {notificationCount} ) : undefined}