diff options
Diffstat (limited to 'src/view/shell/desktop/Search.tsx')
-rw-r--r-- | src/view/shell/desktop/Search.tsx | 209 |
1 files changed, 153 insertions, 56 deletions
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index caecea4a8..f899431b6 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -1,56 +1,150 @@ import React from 'react' -import {TextInput, View, StyleSheet, TouchableOpacity} from 'react-native' +import { + ViewStyle, + TextInput, + View, + StyleSheet, + TouchableOpacity, + ActivityIndicator, +} from 'react-native' import {useNavigation, StackActions} from '@react-navigation/native' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' +import { + AppBskyActorDefs, + moderateProfile, + ProfileModeration, +} from '@atproto/api' +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 {UserAvatar} from '#/view/com/util/UserAvatar' +import {useActorAutocompleteFn} 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') - const textInput = React.useRef<TextInput>(null) - const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) - const [query, setQuery] = React.useState<string>('') - const autocompleteView = React.useMemo<UserAutocompleteModel>( - () => new UserAutocompleteModel(store), - [store], + + 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 function DesktopSearch() { + const {_} = useLingui() + const pal = usePalette('default') const navigation = useNavigation<NavigationProp>() + const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>( + undefined, + ) + const [isActive, setIsActive] = React.useState<boolean>(false) + const [isFetching, setIsFetching] = React.useState<boolean>(false) + const [query, setQuery] = React.useState<string>('') + 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 = useActorAutocompleteFn() - 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 (text.length > 0) { + setIsFetching(true) + setIsActive(true) + + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + + searchDebounceTimeout.current = setTimeout(async () => { + const results = await search({query: text}) + + if (results) { + setSearchResults(results) + setIsFetching(false) + } + }, 300) } else { - autocompleteView.setActive(false) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + setSearchResults([]) + setIsFetching(false) + setIsActive(false) } }, - [setQuery, autocompleteView, isInputFocused], + [setQuery, search, setSearchResults], ) const onPressCancelSearch = React.useCallback(() => { setQuery('') - autocompleteView.setActive(false) - }, [setQuery, autocompleteView]) - + setIsActive(false) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + }, [setQuery]) const onSubmit = React.useCallback(() => { + setIsActive(false) + if (!query.length) return + setSearchResults([]) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) navigation.dispatch(StackActions.push('Search', {q: query})) - autocompleteView.setActive(false) - }, [query, navigation, autocompleteView]) + }, [query, navigation, setSearchResults]) return ( <View style={[styles.container, pal.view]}> @@ -63,19 +157,16 @@ export const DesktopSearch = observer(function DesktopSearch() { /> <TextInput testID="searchTextInput" - ref={textInput} - placeholder="Search" + placeholder={_(msg`Search`)} placeholderTextColor={pal.colors.textLight} selectTextOnFocus returnKeyType="search" value={query} style={[pal.textLight, styles.input]} - onFocus={() => setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - onChangeText={onChangeQuery} + onChangeText={onChangeText} onSubmitEditing={onSubmit} accessibilityRole="search" - accessibilityLabel="Search" + accessibilityLabel={_(msg`Search`)} accessibilityHint="" /> {query ? ( @@ -83,11 +174,11 @@ export const DesktopSearch = observer(function DesktopSearch() { <TouchableOpacity onPress={onPressCancelSearch} accessibilityRole="button" - accessibilityLabel="Cancel search" + accessibilityLabel={_(msg`Cancel search`)} accessibilityHint="Exits inputting search query" onAccessibilityEscape={onPressCancelSearch}> <Text type="lg" style={[pal.link]}> - Cancel + <Trans>Cancel</Trans> </Text> </TouchableOpacity> </View> @@ -95,32 +186,42 @@ export const DesktopSearch = observer(function DesktopSearch() { </View> </View> - {query !== '' && ( + {query !== '' && isActive && moderationOpts && ( <View style={[pal.view, pal.borderDark, styles.resultsContainer]}> - {autocompleteView.suggestions.length ? ( + {isFetching ? ( + <View style={{padding: 8}}> + <ActivityIndicator /> + </View> + ) : ( <> - {autocompleteView.suggestions.map((item, i) => ( - <ProfileCard key={item.did} profile={item} noBorder={i === 0} /> - ))} + {searchResults.length ? ( + 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 {query}</Trans> + </Text> + </View> + )} </> - ) : ( - <View> - <Text style={[pal.textLight, styles.noResults]}> - No results found for {autocompleteView.prefix} - </Text> - </View> )} </View> )} </View> ) -}) +} const styles = StyleSheet.create({ container: { position: 'relative', width: 300, - paddingBottom: 18, }, search: { paddingHorizontal: 16, @@ -150,15 +251,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', |