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
This commit is contained in:
Eric Bailey 2023-11-14 19:51:23 -06:00 committed by GitHub
parent ab1ce078ec
commit d1cb74febe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 186 additions and 49 deletions

View file

@ -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 {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'
export const DesktopSearch = observer(function DesktopSearch() {
const store = useStores()
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 {Text} from 'view/com/util/text/Text'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {useActorSearch} from '#/state/queries/actor-autocomplete'
import {useModerationOpts} from '#/state/queries/preferences'
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',