about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorOllie H <renahlee@outlook.com>2023-05-09 10:13:23 -0700
committerGitHub <noreply@github.com>2023-05-09 12:13:23 -0500
commit8f6b5d3df9b5a5bb61514497f3f25289513ef119 (patch)
treee3b16b8aa3f9cb54595c17e78b3ebb18e3b2ae55 /src
parent9a91b0c538e3c24a25285ae2bdf1d0dfd2ba53a4 (diff)
downloadvoidsky-8f6b5d3df9b5a5bb61514497f3f25289513ef119.tar.zst
Add avatar to mobile autocomplete and create grapheme hook (#602)
* Add avatar to mobile autocomplete and create grapheme hook

* Remove comment, update filename, cut out redundant logic
Diffstat (limited to 'src')
-rw-r--r--src/view/com/composer/text-input/hooks/useGrapheme.tsx36
-rw-r--r--src/view/com/composer/text-input/mobile/Autocomplete.tsx110
-rw-r--r--src/view/com/composer/text-input/web/Autocomplete.tsx30
3 files changed, 110 insertions, 66 deletions
diff --git a/src/view/com/composer/text-input/hooks/useGrapheme.tsx b/src/view/com/composer/text-input/hooks/useGrapheme.tsx
new file mode 100644
index 000000000..25947c3ec
--- /dev/null
+++ b/src/view/com/composer/text-input/hooks/useGrapheme.tsx
@@ -0,0 +1,36 @@
+import Graphemer from 'graphemer'
+import {useCallback, useMemo} from 'react'
+
+export const useGrapheme = () => {
+  const splitter = useMemo(() => new Graphemer(), [])
+
+  const getGraphemeString = useCallback(
+    (name: string, length: number) => {
+      let remainingCharacters = 0
+
+      if (name.length > length) {
+        const graphemes = splitter.splitGraphemes(name)
+
+        if (graphemes.length > length) {
+          remainingCharacters = 0
+          name = `${graphemes.slice(0, length).join('')}...`
+        } else {
+          remainingCharacters = length - graphemes.length
+          name = graphemes.join('')
+        }
+      } else {
+        remainingCharacters = length - name.length
+      }
+
+      return {
+        name,
+        remainingCharacters,
+      }
+    },
+    [splitter],
+  )
+
+  return {
+    getGraphemeString,
+  }
+}
diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
index 7806241f1..c9b8b84b1 100644
--- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
@@ -5,6 +5,8 @@ import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 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'
 
 export const Autocomplete = observer(
   ({
@@ -16,6 +18,7 @@ export const Autocomplete = observer(
   }) => {
     const pal = usePalette('default')
     const positionInterp = useAnimatedValue(0)
+    const {getGraphemeString} = useGrapheme()
 
     useEffect(() => {
       Animated.timing(positionInterp, {
@@ -35,58 +38,83 @@ export const Autocomplete = observer(
         },
       ],
     }
+
     return (
-      <View style={[styles.container, view.isActive && styles.visible]}>
-        <Animated.View
-          style={[
-            styles.animatedContainer,
-            pal.view,
-            pal.border,
-            topAnimStyle,
-            view.isActive && styles.visible,
-          ]}>
-          {view.suggestions.slice(0, 5).map(item => (
-            <TouchableOpacity
-              testID="autocompleteButton"
-              key={item.handle}
-              style={[pal.border, styles.item]}
-              onPress={() => onSelect(item.handle)}
-              accessibilityLabel={`Select ${item.handle}`}
-              accessibilityHint={`Autocompletes to ${item.handle}`}>
-              <Text type="md-medium" style={pal.text}>
-                {item.displayName || item.handle}
-                <Text type="sm" style={pal.textLight}>
-                  &nbsp;@{item.handle}
-                </Text>
+      <Animated.View style={topAnimStyle}>
+        {view.isActive ? (
+          <View style={[pal.view, styles.container, pal.border]}>
+            {view.suggestions.length > 0 ? (
+              view.suggestions.slice(0, 5).map(item => {
+                // 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)
+
+                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} />
+                      <Text type="md-medium" style={pal.text}>
+                        {displayName}
+                      </Text>
+                    </View>
+                    <Text type="sm" style={pal.textLight} numberOfLines={1}>
+                      @{displayHandle}
+                    </Text>
+                  </TouchableOpacity>
+                )
+              })
+            ) : (
+              <Text type="sm" style={[pal.text, pal.border, styles.noResults]}>
+                No result
               </Text>
-            </TouchableOpacity>
-          ))}
-        </Animated.View>
-      </View>
+            )}
+          </View>
+        ) : null}
+      </Animated.View>
     )
   },
 )
 
 const styles = StyleSheet.create({
   container: {
-    display: 'none',
-    height: 250,
-  },
-  animatedContainer: {
-    display: 'none',
-    position: 'absolute',
-    left: -64,
-    right: 0,
-    top: 0,
+    marginLeft: -54,
+    top: 10,
     borderTopWidth: 1,
   },
-  visible: {
-    display: 'flex',
-  },
   item: {
     borderBottomWidth: 1,
-    paddingVertical: 16,
-    paddingHorizontal: 16,
-    height: 50,
+    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/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx
index 20dbbbbe8..475ec119b 100644
--- a/src/view/com/composer/text-input/web/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/web/Autocomplete.tsx
@@ -1,9 +1,7 @@
 import React, {
   forwardRef,
-  useCallback,
   useEffect,
   useImperativeHandle,
-  useMemo,
   useState,
 } from 'react'
 import {StyleSheet, View} from 'react-native'
@@ -16,9 +14,9 @@ import {
 } from '@tiptap/suggestion'
 import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {usePalette} from 'lib/hooks/usePalette'
-import Graphemer from 'graphemer'
 import {Text} from 'view/com/util/text/Text'
 import {UserAvatar} from 'view/com/util/UserAvatar'
+import {useGrapheme} from '../hooks/useGrapheme'
 
 interface MentionListRef {
   onKeyDown: (props: SuggestionKeyDownProps) => boolean
@@ -99,7 +97,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
   (props: SuggestionProps, ref) => {
     const [selectedIndex, setSelectedIndex] = useState(0)
     const pal = usePalette('default')
-    const splitter = useMemo(() => new Graphemer(), [])
+    const {getGraphemeString} = useGrapheme()
 
     const selectItem = (index: number) => {
       const item = props.items[index]
@@ -148,32 +146,14 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
 
     const {items} = props
 
-    const getDisplayedName = useCallback(
-      (name: string) => {
-        // Heuristic value based on max display name and handle lengths
-        const DISPLAY_LIMIT = 30
-        if (name.length > DISPLAY_LIMIT) {
-          const graphemes = splitter.splitGraphemes(name)
-
-          if (graphemes.length > DISPLAY_LIMIT) {
-            return graphemes.length > DISPLAY_LIMIT
-              ? `${graphemes.slice(0, DISPLAY_LIMIT).join('')}...`
-              : name.substring(0, DISPLAY_LIMIT)
-          }
-        }
-
-        return name
-      },
-      [splitter],
-    )
-
     return (
       <div className="items">
         <View style={[pal.borderDark, pal.view, styles.container]}>
           {items.length > 0 ? (
             items.map((item, index) => {
-              const displayName = getDisplayedName(
+              const {name: displayName} = getGraphemeString(
                 item.displayName ?? item.handle,
+                30, // Heuristic value; can be modified
               )
               const isSelected = selectedIndex === index
 
@@ -197,7 +177,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
                     </Text>
                   </View>
                   <Text type="xs" style={pal.textLight} numberOfLines={1}>
-                    {item.handle}
+                    @{item.handle}
                   </Text>
                 </View>
               )