From 587c0c625752964d8ce64faf1d329dce3c834a5c Mon Sep 17 00:00:00 2001
From: Hailey <me@haileyok.com>
Date: Fri, 27 Sep 2024 15:26:28 -0700
Subject: Rework native autocomplete (#5521)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
---
 .../composer/text-input/mobile/Autocomplete.tsx    | 195 ++++++++++-----------
 1 file changed, 88 insertions(+), 107 deletions(-)

(limited to 'src/view/com/composer/text-input/mobile/Autocomplete.tsx')

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,
-  },
-})
-- 
cgit 1.4.1

