diff options
author | Eric Bailey <git@esb.lol> | 2023-11-14 19:51:23 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-14 17:51:23 -0800 |
commit | d1cb74febea9725b028dca372e1b7d7e157ad24d (patch) | |
tree | 6d08f29a709c024e01b543b179bf33b28859eb3c /src/view/shell/desktop/Search.tsx | |
parent | ab1ce078ec16866c691eac81f7b1517bb26399c9 (diff) | |
download | voidsky-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.tsx | 163 |
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', |