about summary refs log tree commit diff
path: root/src/view/shell/desktop/Search.tsx
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2023-11-14 19:51:23 -0600
committerGitHub <noreply@github.com>2023-11-14 17:51:23 -0800
commitd1cb74febea9725b028dca372e1b7d7e157ad24d (patch)
tree6d08f29a709c024e01b543b179bf33b28859eb3c /src/view/shell/desktop/Search.tsx
parentab1ce078ec16866c691eac81f7b1517bb26399c9 (diff)
downloadvoidsky-d1cb74febea9725b028dca372e1b7d7e157ad24d.tar.zst
Desktop user autocomplete search (#1906)
* Fix notification provider order, add comments

* Remove log

* Add actor typeahead handling

* Trim down desktop search styles and hooks

* Clean up moderation
Diffstat (limited to 'src/view/shell/desktop/Search.tsx')
-rw-r--r--src/view/shell/desktop/Search.tsx163
1 files changed, 119 insertions, 44 deletions
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index f54858b8a..115e0f7ae 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -1,59 +1,136 @@
 import React from 'react'
-import {TextInput, View, StyleSheet, TouchableOpacity} from 'react-native'
+import {
+  ViewStyle,
+  TextInput,
+  View,
+  StyleSheet,
+  TouchableOpacity,
+} from 'react-native'
 import {useNavigation, StackActions} from '@react-navigation/native'
-import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
+import {
+  AppBskyActorDefs,
+  moderateProfile,
+  ProfileModeration,
+} from '@atproto/api'
 import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {s} from '#/lib/styles'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {makeProfileLink} from '#/lib/routes/links'
+import {Link} from '#/view/com/util/Link'
 import {usePalette} from 'lib/hooks/usePalette'
 import {MagnifyingGlassIcon2} from 'lib/icons'
 import {NavigationProp} from 'lib/routes/types'
-import {ProfileCard} from 'view/com/profile/ProfileCard'
 import {Text} from 'view/com/util/text/Text'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {useActorSearch} from '#/state/queries/actor-autocomplete'
+import {useModerationOpts} from '#/state/queries/preferences'
 
-export const DesktopSearch = observer(function DesktopSearch() {
-  const store = useStores()
+export function SearchResultCard({
+  profile,
+  style,
+  moderation,
+}: {
+  profile: AppBskyActorDefs.ProfileViewBasic
+  style: ViewStyle
+  moderation: ProfileModeration
+}) {
   const pal = usePalette('default')
+
+  return (
+    <Link
+      href={makeProfileLink(profile)}
+      title={profile.handle}
+      asAnchor
+      anchorNoUnderline>
+      <View
+        style={[
+          pal.border,
+          style,
+          {
+            borderTopWidth: 1,
+            flexDirection: 'row',
+            alignItems: 'center',
+            gap: 12,
+            paddingVertical: 8,
+            paddingHorizontal: 12,
+          },
+        ]}>
+        <UserAvatar
+          size={40}
+          avatar={profile.avatar}
+          moderation={moderation.avatar}
+        />
+        <View style={{flex: 1}}>
+          <Text
+            type="lg"
+            style={[s.bold, pal.text]}
+            numberOfLines={1}
+            lineHeight={1.2}>
+            {sanitizeDisplayName(
+              profile.displayName || sanitizeHandle(profile.handle),
+              moderation.profile,
+            )}
+          </Text>
+          <Text type="md" style={[pal.textLight]} numberOfLines={1}>
+            {sanitizeHandle(profile.handle, '@')}
+          </Text>
+        </View>
+      </View>
+    </Link>
+  )
+}
+
+export const DesktopSearch = observer(function DesktopSearch() {
   const {_} = useLingui()
-  const textInput = React.useRef<TextInput>(null)
+  const pal = usePalette('default')
+  const navigation = useNavigation<NavigationProp>()
+  const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>(
+    undefined,
+  )
   const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
   const [query, setQuery] = React.useState<string>('')
-  const autocompleteView = React.useMemo<UserAutocompleteModel>(
-    () => new UserAutocompleteModel(store),
-    [store],
-  )
-  const navigation = useNavigation<NavigationProp>()
+  const [searchResults, setSearchResults] = React.useState<
+    AppBskyActorDefs.ProfileViewBasic[]
+  >([])
 
-  // initial setup
-  React.useEffect(() => {
-    if (store.me.did) {
-      autocompleteView.setup()
-    }
-  }, [autocompleteView, store.me.did])
+  const moderationOpts = useModerationOpts()
+  const search = useActorSearch()
 
-  const onChangeQuery = React.useCallback(
-    (text: string) => {
+  const onChangeText = React.useCallback(
+    async (text: string) => {
       setQuery(text)
+
       if (text.length > 0 && isInputFocused) {
-        autocompleteView.setActive(true)
-        autocompleteView.setPrefix(text)
+        if (searchDebounceTimeout.current)
+          clearTimeout(searchDebounceTimeout.current)
+
+        searchDebounceTimeout.current = setTimeout(async () => {
+          const results = await search({query: text})
+
+          if (results) {
+            setSearchResults(results)
+          }
+        }, 300)
       } else {
-        autocompleteView.setActive(false)
+        if (searchDebounceTimeout.current)
+          clearTimeout(searchDebounceTimeout.current)
+        setSearchResults([])
       }
     },
-    [setQuery, autocompleteView, isInputFocused],
+    [setQuery, isInputFocused, search, setSearchResults],
   )
 
   const onPressCancelSearch = React.useCallback(() => {
-    setQuery('')
-    autocompleteView.setActive(false)
-  }, [setQuery, autocompleteView])
+    onChangeText('')
+  }, [onChangeText])
 
   const onSubmit = React.useCallback(() => {
     navigation.dispatch(StackActions.push('Search', {q: query}))
-    autocompleteView.setActive(false)
-  }, [query, navigation, autocompleteView])
+  }, [query, navigation])
 
   return (
     <View style={[styles.container, pal.view]}>
@@ -66,7 +143,6 @@ export const DesktopSearch = observer(function DesktopSearch() {
           />
           <TextInput
             testID="searchTextInput"
-            ref={textInput}
             placeholder="Search"
             placeholderTextColor={pal.colors.textLight}
             selectTextOnFocus
@@ -75,7 +151,7 @@ export const DesktopSearch = observer(function DesktopSearch() {
             style={[pal.textLight, styles.input]}
             onFocus={() => setIsInputFocused(true)}
             onBlur={() => setIsInputFocused(false)}
-            onChangeText={onChangeQuery}
+            onChangeText={onChangeText}
             onSubmitEditing={onSubmit}
             accessibilityRole="search"
             accessibilityLabel={_(msg`Search`)}
@@ -100,16 +176,19 @@ export const DesktopSearch = observer(function DesktopSearch() {
 
       {query !== '' && (
         <View style={[pal.view, pal.borderDark, styles.resultsContainer]}>
-          {autocompleteView.suggestions.length ? (
-            <>
-              {autocompleteView.suggestions.map((item, i) => (
-                <ProfileCard key={item.did} profile={item} noBorder={i === 0} />
-              ))}
-            </>
+          {searchResults.length && moderationOpts ? (
+            searchResults.map((item, i) => (
+              <SearchResultCard
+                key={item.did}
+                profile={item}
+                moderation={moderateProfile(item, moderationOpts)}
+                style={i === 0 ? {borderTopWidth: 0} : {}}
+              />
+            ))
           ) : (
             <View>
               <Text style={[pal.textLight, styles.noResults]}>
-                <Trans>No results found for {autocompleteView.prefix}</Trans>
+                <Trans>No results found for {query}</Trans>
               </Text>
             </View>
           )}
@@ -153,15 +232,11 @@ const styles = StyleSheet.create({
     paddingVertical: 7,
   },
   resultsContainer: {
-    // @ts-ignore supported by web
-    // position: 'fixed',
     marginTop: 10,
-
     flexDirection: 'column',
     width: 300,
     borderWidth: 1,
     borderRadius: 6,
-    paddingVertical: 4,
   },
   noResults: {
     textAlign: 'center',