Improve search screen perf (#3752)
* Extract SearchHistory to a component * Extract AutocompleteResults to a component * Extract SearchInputBox to a component * Add a bunch of memoization * Optimize switching by rendering both * Remove subdomain matching This is only ever useful if you type it exactly correct. Search now does a better job anyway. * Give recent search decent hitslopszio/stable
parent
3c2d73909b
commit
5d715ae1d0
|
@ -49,11 +49,7 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||||
import {
|
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
|
||||||
MATCH_HANDLE,
|
|
||||||
SearchLinkCard,
|
|
||||||
SearchProfileCard,
|
|
||||||
} from '#/view/shell/desktop/Search'
|
|
||||||
import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
|
import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
|
||||||
|
@ -156,7 +152,7 @@ function useSuggestedFollows(): [
|
||||||
return [items, onEndReached]
|
return [items, onEndReached]
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchScreenSuggestedFollows() {
|
let SearchScreenSuggestedFollows = (_props: {}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [suggestions, onEndReached] = useSuggestedFollows()
|
const [suggestions, onEndReached] = useSuggestedFollows()
|
||||||
|
|
||||||
|
@ -180,6 +176,7 @@ function SearchScreenSuggestedFollows() {
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SearchScreenSuggestedFollows = React.memo(SearchScreenSuggestedFollows)
|
||||||
|
|
||||||
type SearchResultSlice =
|
type SearchResultSlice =
|
||||||
| {
|
| {
|
||||||
|
@ -192,7 +189,7 @@ type SearchResultSlice =
|
||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchScreenPostResults({
|
let SearchScreenPostResults = ({
|
||||||
query,
|
query,
|
||||||
sort,
|
sort,
|
||||||
active,
|
active,
|
||||||
|
@ -200,7 +197,7 @@ function SearchScreenPostResults({
|
||||||
query: string
|
query: string
|
||||||
sort?: 'top' | 'latest'
|
sort?: 'top' | 'latest'
|
||||||
active: boolean
|
active: boolean
|
||||||
}) {
|
}): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const [isPTR, setIsPTR] = React.useState(false)
|
const [isPTR, setIsPTR] = React.useState(false)
|
||||||
|
@ -298,14 +295,15 @@ function SearchScreenPostResults({
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SearchScreenPostResults = React.memo(SearchScreenPostResults)
|
||||||
|
|
||||||
function SearchScreenUserResults({
|
let SearchScreenUserResults = ({
|
||||||
query,
|
query,
|
||||||
active,
|
active,
|
||||||
}: {
|
}: {
|
||||||
query: string
|
query: string
|
||||||
active: boolean
|
active: boolean
|
||||||
}) {
|
}): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const {data: results, isFetched} = useActorSearch({
|
const {data: results, isFetched} = useActorSearch({
|
||||||
|
@ -334,8 +332,9 @@ function SearchScreenUserResults({
|
||||||
<Loader />
|
<Loader />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SearchScreenUserResults = React.memo(SearchScreenUserResults)
|
||||||
|
|
||||||
export function SearchScreenInner({query}: {query?: string}) {
|
let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||||
|
@ -467,18 +466,17 @@ export function SearchScreenInner({query}: {query?: string}) {
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SearchScreenInner = React.memo(SearchScreenInner)
|
||||||
|
|
||||||
export function SearchScreen(
|
export function SearchScreen(
|
||||||
props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
|
props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
|
||||||
) {
|
) {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const theme = useTheme()
|
|
||||||
const textInput = React.useRef<TextInput>(null)
|
const textInput = React.useRef<TextInput>(null)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const setDrawerOpen = useSetDrawerOpen()
|
const setDrawerOpen = useSetDrawerOpen()
|
||||||
const moderationOpts = useModerationOpts()
|
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries()
|
const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries()
|
||||||
|
|
||||||
|
@ -584,21 +582,27 @@ export function SearchScreen(
|
||||||
navigateToItem(searchText)
|
navigateToItem(searchText)
|
||||||
}, [navigateToItem, searchText])
|
}, [navigateToItem, searchText])
|
||||||
|
|
||||||
const handleHistoryItemClick = (item: string) => {
|
const onAutocompleteResultPress = React.useCallback(() => {
|
||||||
setSearchText(item)
|
if (isWeb) {
|
||||||
navigateToItem(item)
|
setShowAutocomplete(false)
|
||||||
}
|
} else {
|
||||||
|
textInput.current?.blur()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleHistoryItemClick = React.useCallback(
|
||||||
|
(item: string) => {
|
||||||
|
setSearchText(item)
|
||||||
|
navigateToItem(item)
|
||||||
|
},
|
||||||
|
[navigateToItem],
|
||||||
|
)
|
||||||
|
|
||||||
const onSoftReset = React.useCallback(() => {
|
const onSoftReset = React.useCallback(() => {
|
||||||
scrollToTopWeb()
|
scrollToTopWeb()
|
||||||
onPressCancelSearch()
|
onPressCancelSearch()
|
||||||
}, [onPressCancelSearch])
|
}, [onPressCancelSearch])
|
||||||
|
|
||||||
const queryMaybeHandle = React.useMemo(() => {
|
|
||||||
const match = MATCH_HANDLE.exec(queryParam)
|
|
||||||
return match && match[1]
|
|
||||||
}, [queryParam])
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
setMinimalShellMode(false)
|
setMinimalShellMode(false)
|
||||||
|
@ -606,15 +610,19 @@ export function SearchScreen(
|
||||||
}, [onSoftReset, setMinimalShellMode]),
|
}, [onSoftReset, setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleRemoveHistoryItem = (itemToRemove: string) => {
|
const handleRemoveHistoryItem = React.useCallback(
|
||||||
const updatedHistory = searchHistory.filter(item => item !== itemToRemove)
|
(itemToRemove: string) => {
|
||||||
setSearchHistory(updatedHistory)
|
const updatedHistory = searchHistory.filter(item => item !== itemToRemove)
|
||||||
AsyncStorage.setItem('searchHistory', JSON.stringify(updatedHistory)).catch(
|
setSearchHistory(updatedHistory)
|
||||||
e => {
|
AsyncStorage.setItem(
|
||||||
|
'searchHistory',
|
||||||
|
JSON.stringify(updatedHistory),
|
||||||
|
).catch(e => {
|
||||||
logger.error('Failed to update search history', {message: e})
|
logger.error('Failed to update search history', {message: e})
|
||||||
},
|
})
|
||||||
)
|
},
|
||||||
}
|
[searchHistory],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={isWeb ? null : {flex: 1}}>
|
<View style={isWeb ? null : {flex: 1}}>
|
||||||
|
@ -642,81 +650,15 @@ export function SearchScreen(
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
|
<SearchInputBox
|
||||||
<Pressable
|
textInput={textInput}
|
||||||
// This only exists only for extra hitslop so don't expose it to the a11y tree.
|
searchText={searchText}
|
||||||
accessible={false}
|
showAutocomplete={showAutocomplete}
|
||||||
focusable={false}
|
setShowAutocomplete={setShowAutocomplete}
|
||||||
// @ts-ignore web-only
|
onChangeText={onChangeText}
|
||||||
tabIndex={-1}
|
onSubmit={onSubmit}
|
||||||
style={[
|
onPressClearQuery={onPressClearQuery}
|
||||||
{backgroundColor: pal.colors.backgroundLight},
|
/>
|
||||||
styles.headerSearchContainer,
|
|
||||||
isWeb && {
|
|
||||||
// @ts-ignore web only
|
|
||||||
cursor: 'default',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPress={() => {
|
|
||||||
textInput.current?.focus()
|
|
||||||
}}>
|
|
||||||
<MagnifyingGlassIcon
|
|
||||||
style={[pal.icon, styles.headerSearchIcon]}
|
|
||||||
size={21}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="searchTextInput"
|
|
||||||
ref={textInput}
|
|
||||||
placeholder={_(msg`Search`)}
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
returnKeyType="search"
|
|
||||||
value={searchText}
|
|
||||||
style={[pal.text, styles.headerSearchInput]}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
selectTextOnFocus={isNative}
|
|
||||||
onFocus={() => {
|
|
||||||
if (isWeb) {
|
|
||||||
// Prevent a jump on iPad by ensuring that
|
|
||||||
// the initial focused render has no result list.
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setShowAutocomplete(true)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setShowAutocomplete(true)
|
|
||||||
if (isIOS) {
|
|
||||||
// We rely on selectTextOnFocus, but it's broken on iOS:
|
|
||||||
// https://github.com/facebook/react-native/issues/41988
|
|
||||||
textInput.current?.setSelection(0, searchText.length)
|
|
||||||
// We still rely on selectTextOnFocus for it to be instant on Android.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
onSubmitEditing={onSubmit}
|
|
||||||
autoFocus={false}
|
|
||||||
accessibilityRole="search"
|
|
||||||
accessibilityLabel={_(msg`Search`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
autoCorrect={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
|
||||||
{showAutocomplete && searchText.length > 0 && (
|
|
||||||
<Pressable
|
|
||||||
testID="searchTextInputClearBtn"
|
|
||||||
onPress={onPressClearQuery}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Clear search query`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
hitSlop={HITSLOP_10}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="xmark"
|
|
||||||
size={16}
|
|
||||||
style={pal.textLight as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
{showAutocomplete && (
|
{showAutocomplete && (
|
||||||
<View style={[styles.headerCancelBtn]}>
|
<View style={[styles.headerCancelBtn]}>
|
||||||
<Pressable
|
<Pressable
|
||||||
|
@ -730,107 +672,250 @@ export function SearchScreen(
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
|
<View
|
||||||
{showAutocomplete && searchText.length > 0 ? (
|
style={{
|
||||||
<>
|
display: showAutocomplete ? 'flex' : 'none',
|
||||||
{(isAutocompleteFetching && !autocompleteData?.length) ||
|
flex: 1,
|
||||||
!moderationOpts ? (
|
}}>
|
||||||
<Loader />
|
{searchText.length > 0 ? (
|
||||||
) : (
|
<AutocompleteResults
|
||||||
<ScrollView
|
isAutocompleteFetching={isAutocompleteFetching}
|
||||||
style={{height: '100%'}}
|
autocompleteData={autocompleteData}
|
||||||
// @ts-ignore web only -prf
|
searchText={searchText}
|
||||||
dataSet={{stableGutters: '1'}}
|
onSubmit={onSubmit}
|
||||||
keyboardShouldPersistTaps="handled"
|
onResultPress={onAutocompleteResultPress}
|
||||||
keyboardDismissMode="on-drag">
|
/>
|
||||||
<SearchLinkCard
|
) : (
|
||||||
label={_(msg`Search for "${searchText}"`)}
|
<SearchHistory
|
||||||
onPress={isNative ? onSubmit : undefined}
|
searchHistory={searchHistory}
|
||||||
to={
|
onItemClick={handleHistoryItemClick}
|
||||||
isNative
|
onRemoveItemClick={handleRemoveHistoryItem}
|
||||||
? undefined
|
/>
|
||||||
: `/search?q=${encodeURIComponent(searchText)}`
|
)}
|
||||||
}
|
</View>
|
||||||
style={{borderBottomWidth: 1}}
|
<View
|
||||||
/>
|
style={{
|
||||||
|
display: showAutocomplete ? 'none' : 'flex',
|
||||||
{queryMaybeHandle ? (
|
flex: 1,
|
||||||
<SearchLinkCard
|
}}>
|
||||||
label={_(msg`Go to @${queryMaybeHandle}`)}
|
|
||||||
to={`/profile/${queryMaybeHandle}`}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{autocompleteData?.map(item => (
|
|
||||||
<SearchProfileCard
|
|
||||||
key={item.did}
|
|
||||||
profile={item}
|
|
||||||
moderation={moderateProfile(item, moderationOpts)}
|
|
||||||
onPress={() => {
|
|
||||||
if (isWeb) {
|
|
||||||
setShowAutocomplete(false)
|
|
||||||
} else {
|
|
||||||
textInput.current?.blur()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<View style={{height: 200}} />
|
|
||||||
</ScrollView>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : !queryParam && showAutocomplete ? (
|
|
||||||
<CenteredView
|
|
||||||
sideBorders={isTabletOrDesktop}
|
|
||||||
// @ts-ignore web only -prf
|
|
||||||
style={{
|
|
||||||
height: isWeb ? '100vh' : undefined,
|
|
||||||
}}>
|
|
||||||
<View style={styles.searchHistoryContainer}>
|
|
||||||
{searchHistory.length > 0 && (
|
|
||||||
<View style={styles.searchHistoryContent}>
|
|
||||||
<Text style={[pal.text, styles.searchHistoryTitle]}>
|
|
||||||
<Trans>Recent Searches</Trans>
|
|
||||||
</Text>
|
|
||||||
{searchHistory.map((historyItem, index) => (
|
|
||||||
<View
|
|
||||||
key={index}
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.mt_md,
|
|
||||||
a.justify_center,
|
|
||||||
a.justify_between,
|
|
||||||
]}>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
onPress={() => handleHistoryItemClick(historyItem)}
|
|
||||||
style={[a.flex_1, a.py_sm]}>
|
|
||||||
<Text style={pal.text}>{historyItem}</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
onPress={() => handleRemoveHistoryItem(historyItem)}
|
|
||||||
style={[a.px_md, a.py_xs, a.justify_center]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="xmark"
|
|
||||||
size={16}
|
|
||||||
style={pal.textLight as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</CenteredView>
|
|
||||||
) : (
|
|
||||||
<SearchScreenInner query={queryParam} />
|
<SearchScreenInner query={queryParam} />
|
||||||
)}
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let SearchInputBox = ({
|
||||||
|
textInput,
|
||||||
|
searchText,
|
||||||
|
showAutocomplete,
|
||||||
|
setShowAutocomplete,
|
||||||
|
onChangeText,
|
||||||
|
onSubmit,
|
||||||
|
onPressClearQuery,
|
||||||
|
}: {
|
||||||
|
textInput: React.RefObject<TextInput>
|
||||||
|
searchText: string
|
||||||
|
showAutocomplete: boolean
|
||||||
|
setShowAutocomplete: (show: boolean) => void
|
||||||
|
onChangeText: (text: string) => void
|
||||||
|
onSubmit: () => void
|
||||||
|
onPressClearQuery: () => void
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
const theme = useTheme()
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
// This only exists only for extra hitslop so don't expose it to the a11y tree.
|
||||||
|
accessible={false}
|
||||||
|
focusable={false}
|
||||||
|
// @ts-ignore web-only
|
||||||
|
tabIndex={-1}
|
||||||
|
style={[
|
||||||
|
{backgroundColor: pal.colors.backgroundLight},
|
||||||
|
styles.headerSearchContainer,
|
||||||
|
isWeb && {
|
||||||
|
// @ts-ignore web only
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPress={() => {
|
||||||
|
textInput.current?.focus()
|
||||||
|
}}>
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
style={[pal.icon, styles.headerSearchIcon]}
|
||||||
|
size={21}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="searchTextInput"
|
||||||
|
ref={textInput}
|
||||||
|
placeholder={_(msg`Search`)}
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
returnKeyType="search"
|
||||||
|
value={searchText}
|
||||||
|
style={[pal.text, styles.headerSearchInput]}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
selectTextOnFocus={isNative}
|
||||||
|
onFocus={() => {
|
||||||
|
if (isWeb) {
|
||||||
|
// Prevent a jump on iPad by ensuring that
|
||||||
|
// the initial focused render has no result list.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setShowAutocomplete(true)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setShowAutocomplete(true)
|
||||||
|
if (isIOS) {
|
||||||
|
// We rely on selectTextOnFocus, but it's broken on iOS:
|
||||||
|
// https://github.com/facebook/react-native/issues/41988
|
||||||
|
textInput.current?.setSelection(0, searchText.length)
|
||||||
|
// We still rely on selectTextOnFocus for it to be instant on Android.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
onSubmitEditing={onSubmit}
|
||||||
|
autoFocus={false}
|
||||||
|
accessibilityRole="search"
|
||||||
|
accessibilityLabel={_(msg`Search`)}
|
||||||
|
accessibilityHint=""
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
{showAutocomplete && searchText.length > 0 && (
|
||||||
|
<Pressable
|
||||||
|
testID="searchTextInputClearBtn"
|
||||||
|
onPress={onPressClearQuery}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Clear search query`)}
|
||||||
|
accessibilityHint=""
|
||||||
|
hitSlop={HITSLOP_10}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="xmark"
|
||||||
|
size={16}
|
||||||
|
style={pal.textLight as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SearchInputBox = React.memo(SearchInputBox)
|
||||||
|
|
||||||
|
let AutocompleteResults = ({
|
||||||
|
isAutocompleteFetching,
|
||||||
|
autocompleteData,
|
||||||
|
searchText,
|
||||||
|
onSubmit,
|
||||||
|
onResultPress,
|
||||||
|
}: {
|
||||||
|
isAutocompleteFetching: boolean
|
||||||
|
autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined
|
||||||
|
searchText: string
|
||||||
|
onSubmit: () => void
|
||||||
|
onResultPress: () => void
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const {_} = useLingui()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{(isAutocompleteFetching && !autocompleteData?.length) ||
|
||||||
|
!moderationOpts ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
<ScrollView
|
||||||
|
style={{height: '100%'}}
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
dataSet={{stableGutters: '1'}}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
keyboardDismissMode="on-drag">
|
||||||
|
<SearchLinkCard
|
||||||
|
label={_(msg`Search for "${searchText}"`)}
|
||||||
|
onPress={isNative ? onSubmit : undefined}
|
||||||
|
to={
|
||||||
|
isNative
|
||||||
|
? undefined
|
||||||
|
: `/search?q=${encodeURIComponent(searchText)}`
|
||||||
|
}
|
||||||
|
style={{borderBottomWidth: 1}}
|
||||||
|
/>
|
||||||
|
{autocompleteData?.map(item => (
|
||||||
|
<SearchProfileCard
|
||||||
|
key={item.did}
|
||||||
|
profile={item}
|
||||||
|
moderation={moderateProfile(item, moderationOpts)}
|
||||||
|
onPress={onResultPress}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<View style={{height: 200}} />
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AutocompleteResults = React.memo(AutocompleteResults)
|
||||||
|
|
||||||
|
function SearchHistory({
|
||||||
|
searchHistory,
|
||||||
|
onItemClick,
|
||||||
|
onRemoveItemClick,
|
||||||
|
}: {
|
||||||
|
searchHistory: string[]
|
||||||
|
onItemClick: (item: string) => void
|
||||||
|
onRemoveItemClick: (item: string) => void
|
||||||
|
}) {
|
||||||
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<CenteredView
|
||||||
|
sideBorders={isTabletOrDesktop}
|
||||||
|
// @ts-ignore web only -prf
|
||||||
|
style={{
|
||||||
|
height: isWeb ? '100vh' : undefined,
|
||||||
|
}}>
|
||||||
|
<View style={styles.searchHistoryContainer}>
|
||||||
|
{searchHistory.length > 0 && (
|
||||||
|
<View style={styles.searchHistoryContent}>
|
||||||
|
<Text style={[pal.text, styles.searchHistoryTitle]}>
|
||||||
|
<Trans>Recent Searches</Trans>
|
||||||
|
</Text>
|
||||||
|
{searchHistory.map((historyItem, index) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.mt_md,
|
||||||
|
a.justify_center,
|
||||||
|
a.justify_between,
|
||||||
|
]}>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={() => onItemClick(historyItem)}
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
style={[a.flex_1, a.py_sm]}>
|
||||||
|
<Text style={pal.text}>{historyItem}</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={() => onRemoveItemClick(historyItem)}
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
style={[a.px_md, a.py_xs, a.justify_center]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="xmark"
|
||||||
|
size={16}
|
||||||
|
style={pal.textLight as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function scrollToTopWeb() {
|
function scrollToTopWeb() {
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
|
|
|
@ -31,10 +31,7 @@ import {Link} from '#/view/com/util/Link'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
|
||||||
export const MATCH_HANDLE =
|
let SearchLinkCard = ({
|
||||||
/@?([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))/
|
|
||||||
|
|
||||||
export function SearchLinkCard({
|
|
||||||
label,
|
label,
|
||||||
to,
|
to,
|
||||||
onPress,
|
onPress,
|
||||||
|
@ -44,7 +41,7 @@ export function SearchLinkCard({
|
||||||
to?: string
|
to?: string
|
||||||
onPress?: () => void
|
onPress?: () => void
|
||||||
style?: ViewStyle
|
style?: ViewStyle
|
||||||
}) {
|
}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
const inner = (
|
const inner = (
|
||||||
|
@ -82,8 +79,10 @@ export function SearchLinkCard({
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SearchLinkCard = React.memo(SearchLinkCard)
|
||||||
|
export {SearchLinkCard}
|
||||||
|
|
||||||
export function SearchProfileCard({
|
let SearchProfileCard = ({
|
||||||
profile,
|
profile,
|
||||||
moderation,
|
moderation,
|
||||||
onPress: onPressInner,
|
onPress: onPressInner,
|
||||||
|
@ -91,7 +90,7 @@ export function SearchProfileCard({
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
moderation: ModerationDecision
|
moderation: ModerationDecision
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
}) {
|
}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
@ -144,6 +143,8 @@ export function SearchProfileCard({
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SearchProfileCard = React.memo(SearchProfileCard)
|
||||||
|
export {SearchProfileCard}
|
||||||
|
|
||||||
export function DesktopSearch() {
|
export function DesktopSearch() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
@ -179,11 +180,6 @@ export function DesktopSearch() {
|
||||||
setIsActive(false)
|
setIsActive(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const queryMaybeHandle = React.useMemo(() => {
|
|
||||||
const match = MATCH_HANDLE.exec(query)
|
|
||||||
return match && match[1]
|
|
||||||
}, [query])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, pal.view]}>
|
<View style={[styles.container, pal.view]}>
|
||||||
<View
|
<View
|
||||||
|
@ -239,19 +235,11 @@ export function DesktopSearch() {
|
||||||
label={_(msg`Search for "${query}"`)}
|
label={_(msg`Search for "${query}"`)}
|
||||||
to={`/search?q=${encodeURIComponent(query)}`}
|
to={`/search?q=${encodeURIComponent(query)}`}
|
||||||
style={
|
style={
|
||||||
queryMaybeHandle || (autocompleteData?.length ?? 0) > 0
|
(autocompleteData?.length ?? 0) > 0
|
||||||
? {borderBottomWidth: 1}
|
? {borderBottomWidth: 1}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{queryMaybeHandle ? (
|
|
||||||
<SearchLinkCard
|
|
||||||
label={_(msg`Go to @${queryMaybeHandle}`)}
|
|
||||||
to={`/profile/${queryMaybeHandle}`}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{autocompleteData?.map(item => (
|
{autocompleteData?.map(item => (
|
||||||
<SearchProfileCard
|
<SearchProfileCard
|
||||||
key={item.did}
|
key={item.did}
|
||||||
|
|
Loading…
Reference in New Issue