about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--bskyweb/templates/base.html38
-rw-r--r--src/view/com/composer/text-input/web/Autocomplete.tsx122
-rw-r--r--web/index.html22
3 files changed, 118 insertions, 64 deletions
diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html
index 3bc8098ae..1d51b4f20 100644
--- a/bskyweb/templates/base.html
+++ b/bskyweb/templates/base.html
@@ -57,14 +57,6 @@
       }
     }*/
 
-    /* OLLIE: TODO -- this is not accessible */
-    /* Remove focus state on inputs */
-    .ProseMirror-focused {
-      outline: 0;
-    }
-    input:focus {
-      outline: 0;
-    }
     /* Remove default link styling */
     a {
       color: inherit;
@@ -106,28 +98,16 @@
       color: #0085ff;
       cursor: pointer;
     }
+    /* OLLIE: TODO -- this is not accessible */
+    /* Remove focus state on inputs */
+    .ProseMirror-focused {
+      outline: 0;
+    }
+    input:focus {
+      outline: 0;
+    }
     .tippy-content .items {
-      border-radius: 6px;
-      background: #F3F3F8;
-      border: 1px solid #e0d9d9;
-      padding: 3px 3px;
-    }
-    .tippy-content .items .item {
-      display: block;
-      background: transparent;
-      color: #8a8c9a;
-      border: 0;
-      font: 17px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-      padding: 7px 10px 8px;
-      width: 100%;
-      text-align: left;
-      box-sizing: border-box;
-      letter-spacing: 0.2px;
-    }
-    .tippy-content .items .item.is-selected {
-      background: #fff;
-      border-radius: 4px;
-      color: #333;
+      width: fit-content;
     }
   </style>
   {% include "scripts.html" %}
diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx
index 7c6f8770b..20dbbbbe8 100644
--- a/src/view/com/composer/text-input/web/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/web/Autocomplete.tsx
@@ -1,9 +1,12 @@
 import React, {
   forwardRef,
+  useCallback,
   useEffect,
   useImperativeHandle,
+  useMemo,
   useState,
 } from 'react'
+import {StyleSheet, View} from 'react-native'
 import {ReactRenderer} from '@tiptap/react'
 import tippy, {Instance as TippyInstance} from 'tippy.js'
 import {
@@ -12,6 +15,10 @@ import {
   SuggestionKeyDownProps,
 } 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'
 
 interface MentionListRef {
   onKeyDown: (props: SuggestionKeyDownProps) => boolean
@@ -26,7 +33,7 @@ export function createSuggestion({
     async items({query}) {
       autocompleteView.setActive(true)
       await autocompleteView.setPrefix(query)
-      return autocompleteView.suggestions.slice(0, 8).map(s => s.handle)
+      return autocompleteView.suggestions.slice(0, 8)
     },
 
     render: () => {
@@ -91,12 +98,14 @@ export function createSuggestion({
 const MentionList = forwardRef<MentionListRef, SuggestionProps>(
   (props: SuggestionProps, ref) => {
     const [selectedIndex, setSelectedIndex] = useState(0)
+    const pal = usePalette('default')
+    const splitter = useMemo(() => new Graphemer(), [])
 
     const selectItem = (index: number) => {
       const item = props.items[index]
 
       if (item) {
-        props.command({id: item})
+        props.command({id: item.handle})
       }
     }
 
@@ -137,21 +146,106 @@ 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">
-        {props.items.length ? (
-          props.items.map((item, index) => (
-            <button
-              className={`item ${index === selectedIndex ? 'is-selected' : ''}`}
-              key={index}
-              onClick={() => selectItem(index)}>
-              {item}
-            </button>
-          ))
-        ) : (
-          <div className="item">No result</div>
-        )}
+        <View style={[pal.borderDark, pal.view, styles.container]}>
+          {items.length > 0 ? (
+            items.map((item, index) => {
+              const displayName = getDisplayedName(
+                item.displayName ?? item.handle,
+              )
+              const isSelected = selectedIndex === index
+
+              return (
+                <View
+                  key={item.handle}
+                  style={[
+                    isSelected ? pal.viewLight : undefined,
+                    pal.borderDark,
+                    styles.mentionContainer,
+                    index === 0
+                      ? styles.firstMention
+                      : index === items.length - 1
+                      ? styles.lastMention
+                      : undefined,
+                  ]}>
+                  <View style={styles.avatarAndDisplayName}>
+                    <UserAvatar avatar={item.avatar ?? null} size={26} />
+                    <Text style={pal.text} numberOfLines={1}>
+                      {displayName}
+                    </Text>
+                  </View>
+                  <Text type="xs" style={pal.textLight} numberOfLines={1}>
+                    {item.handle}
+                  </Text>
+                </View>
+              )
+            })
+          ) : (
+            <Text type="sm" style={[pal.text, styles.noResult]}>
+              No result
+            </Text>
+          )}
+        </View>
       </div>
     )
   },
 )
+
+const styles = StyleSheet.create({
+  container: {
+    width: 500,
+    borderRadius: 6,
+    borderWidth: 1,
+    borderStyle: 'solid',
+    padding: 4,
+  },
+  mentionContainer: {
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    flexDirection: 'row',
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+    gap: 4,
+  },
+  firstMention: {
+    borderTopLeftRadius: 2,
+    borderTopRightRadius: 2,
+  },
+  lastMention: {
+    borderBottomLeftRadius: 2,
+    borderBottomRightRadius: 2,
+  },
+  avatarAndDisplayName: {
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 6,
+  },
+  noResult: {
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+  },
+})
diff --git a/web/index.html b/web/index.html
index f88fd727b..f518665ca 100644
--- a/web/index.html
+++ b/web/index.html
@@ -110,27 +110,7 @@
         outline: 0;
       }
       .tippy-content .items {
-        border-radius: 6px;
-        background: #F3F3F8;
-        border: 1px solid #e0d9d9;
-        padding: 3px 3px;
-      }
-      .tippy-content .items .item {
-        display: block;
-        background: transparent;
-        color: #8a8c9a;
-        border: 0;
-        font: 17px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-        padding: 7px 10px 8px;
-        width: 100%;
-        text-align: left;
-        box-sizing: border-box;
-        letter-spacing: 0.2px;
-      }
-      .tippy-content .items .item.is-selected {
-        background: #fff;
-        border-radius: 4px;
-        color: #333;
+        width: fit-content;
       }
     </style>
   </head>