Fix search typeahead performance with absolute overlay (#11103)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,11 @@ import {
|
|||||||
View,
|
View,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
FadeInDown,
|
||||||
|
FadeOutDown,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {setStringAsync} from 'expo-clipboard'
|
import {setStringAsync} from 'expo-clipboard'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
|
import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
|
||||||
@@ -41,7 +46,15 @@ import {
|
|||||||
withoutFilterParams,
|
withoutFilterParams,
|
||||||
} from '#/screens/Search/searchParams'
|
} from '#/screens/Search/searchParams'
|
||||||
import {makeSearchQuery} from '#/screens/Search/utils'
|
import {makeSearchQuery} from '#/screens/Search/utils'
|
||||||
import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf'
|
import {
|
||||||
|
atoms as a,
|
||||||
|
native,
|
||||||
|
platform,
|
||||||
|
tokens,
|
||||||
|
useBreakpoints,
|
||||||
|
useTheme,
|
||||||
|
web,
|
||||||
|
} from '#/alf'
|
||||||
import {useAutocomplete} from '#/components/Autocomplete'
|
import {useAutocomplete} from '#/components/Autocomplete'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
||||||
@@ -387,13 +400,14 @@ export function SearchScreenShell({
|
|||||||
|
|
||||||
const handleProfileClick = useCallback(
|
const handleProfileClick = useCallback(
|
||||||
(profile: bsky.profile.AnyProfileView) => {
|
(profile: bsky.profile.AnyProfileView) => {
|
||||||
|
updateSearchText('')
|
||||||
unstableCacheProfileView(queryClient, profile)
|
unstableCacheProfileView(queryClient, profile)
|
||||||
// Slight delay to avoid updating during push nav animation.
|
// Slight delay to avoid updating during push nav animation.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateProfileHistory(profile)
|
updateProfileHistory(profile)
|
||||||
}, 400)
|
}, 400)
|
||||||
},
|
},
|
||||||
[updateProfileHistory, queryClient],
|
[updateProfileHistory, queryClient, updateSearchText],
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -468,6 +482,17 @@ export function SearchScreenShell({
|
|||||||
}
|
}
|
||||||
}, [setShowAutocomplete])
|
}, [setShowAutocomplete])
|
||||||
|
|
||||||
|
const onSearchInputBlur = useCallback(() => {
|
||||||
|
/*
|
||||||
|
* Bind autocomplete visibility to focus state on native. On web this
|
||||||
|
* doesn't work because of focus management, which would render the
|
||||||
|
* autocomplete results uninteractable.
|
||||||
|
*/
|
||||||
|
if (IS_NATIVE) {
|
||||||
|
setShowAutocomplete(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const focusSearchInput = useCallback(
|
const focusSearchInput = useCallback(
|
||||||
(tab?: TabParam) => {
|
(tab?: TabParam) => {
|
||||||
textInput.current?.focus()
|
textInput.current?.focus()
|
||||||
@@ -597,6 +622,7 @@ export function SearchScreenShell({
|
|||||||
ref={textInput}
|
ref={textInput}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onFocus={onSearchInputFocus}
|
onFocus={onSearchInputFocus}
|
||||||
|
onBlur={onSearchInputBlur}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
onClearText={onPressClearQuery}
|
onClearText={onPressClearQuery}
|
||||||
onSubmitEditing={onSubmit('typed')}
|
onSubmitEditing={onSubmit('typed')}
|
||||||
@@ -643,52 +669,57 @@ export function SearchScreenShell({
|
|||||||
</Layout.Center>
|
</Layout.Center>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View style={[a.flex_1, a.relative]}>
|
||||||
style={{
|
<View style={[a.flex_1, web(showAutocomplete && a.hidden)]}>
|
||||||
display: showAutocomplete && !fixedParams ? 'flex' : 'none',
|
<SearchScreenInner
|
||||||
flex: 1,
|
key={filters.lang ?? ''}
|
||||||
}}>
|
activeTab={activeTab}
|
||||||
{searchText.length > 0 && IS_NATIVE ? (
|
setActiveTab={setActiveTab}
|
||||||
<AutocompleteResults
|
query={query}
|
||||||
items={autocompleteItems}
|
queryWithParams={queryWithParams}
|
||||||
isFetching={isAutocompleteFetching}
|
filters={filters}
|
||||||
searchText={searchText}
|
hasFilters={hasFilters}
|
||||||
onSubmit={onSubmit('autocomplete')}
|
headerHeight={headerHeight}
|
||||||
onResultPress={onAutocompleteResultPress}
|
focusSearchInput={focusSearchInput}
|
||||||
onProfileClick={handleProfileClick}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<SearchHistory
|
|
||||||
searchHistory={termHistory}
|
|
||||||
selectedProfiles={
|
|
||||||
accountHistoryProfiles?.profiles.filter(p =>
|
|
||||||
accountHistory.includes(p.did),
|
|
||||||
) ?? []
|
|
||||||
}
|
|
||||||
onItemClick={handleHistoryItemClick}
|
|
||||||
onProfileClick={handleProfileClick}
|
|
||||||
onRemoveItemClick={deleteSearchHistoryItem}
|
|
||||||
onRemoveProfileClick={deleteProfileHistoryItem}
|
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{showAutocomplete && !fixedParams && (
|
||||||
|
<Animated.View
|
||||||
|
entering={native(FadeInDown.easing(Easing.out(Easing.cubic)))}
|
||||||
|
exiting={native(FadeOutDown.easing(Easing.out(Easing.cubic)))}
|
||||||
|
style={platform({
|
||||||
|
web: [a.flex_1],
|
||||||
|
native: [t.atoms.bg, a.absolute, a.inset_0],
|
||||||
|
})}
|
||||||
|
accessibilityViewIsModal
|
||||||
|
accessibilityRole="list">
|
||||||
|
{searchText.length > 0 && IS_NATIVE ? (
|
||||||
|
<AutocompleteResults
|
||||||
|
items={autocompleteItems}
|
||||||
|
isFetching={isAutocompleteFetching}
|
||||||
|
searchText={searchText}
|
||||||
|
onSubmit={onSubmit('autocomplete')}
|
||||||
|
onResultPress={onAutocompleteResultPress}
|
||||||
|
onProfileClick={handleProfileClick}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SearchHistory
|
||||||
|
searchHistory={termHistory}
|
||||||
|
selectedProfiles={
|
||||||
|
accountHistoryProfiles?.profiles.filter(p =>
|
||||||
|
accountHistory.includes(p.did),
|
||||||
|
) ?? []
|
||||||
|
}
|
||||||
|
onItemClick={handleHistoryItemClick}
|
||||||
|
onProfileClick={handleProfileClick}
|
||||||
|
onRemoveItemClick={deleteSearchHistoryItem}
|
||||||
|
onRemoveProfileClick={deleteProfileHistoryItem}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
display: showAutocomplete ? 'none' : 'flex',
|
|
||||||
flex: 1,
|
|
||||||
}}>
|
|
||||||
<SearchScreenInner
|
|
||||||
key={filters.lang ?? ''}
|
|
||||||
activeTab={activeTab}
|
|
||||||
setActiveTab={setActiveTab}
|
|
||||||
query={query}
|
|
||||||
queryWithParams={queryWithParams}
|
|
||||||
filters={filters}
|
|
||||||
hasFilters={hasFilters}
|
|
||||||
headerHeight={headerHeight}
|
|
||||||
focusSearchInput={focusSearchInput}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {Pressable, ScrollView, View} from 'react-native'
|
import {ScrollView, View} from 'react-native'
|
||||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {createHitslop, HITSLOP_10} from '#/lib/constants'
|
import {createHitslop} from '#/lib/constants'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
@@ -89,7 +89,7 @@ export function SearchHistory({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{searchHistory.length > 0 && (
|
{searchHistory.length > 0 && (
|
||||||
<View style={[a.px_lg, a.pt_sm]}>
|
<View style={[a.pt_sm]}>
|
||||||
{searchHistory.slice(0, 5).map((historyItem, index) => {
|
{searchHistory.slice(0, 5).map((historyItem, index) => {
|
||||||
const {q, filters} = parseHistoryEntry(historyItem)
|
const {q, filters} = parseHistoryEntry(historyItem)
|
||||||
const filterCount = countActiveFilters(filters)
|
const filterCount = countActiveFilters(filters)
|
||||||
@@ -132,38 +132,60 @@ function SearchHistoryItem({
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
<View style={[a.flex_row, a.align_center]}>
|
||||||
<Pressable
|
<Button label={l`Search for ${q}`} onPress={onPress} style={[a.flex_1]}>
|
||||||
accessibilityRole="button"
|
{({hovered, focused, pressed}) => (
|
||||||
onPress={onPress}
|
|
||||||
hitSlop={HITSLOP_10}
|
|
||||||
style={[a.flex_1, a.py_sm, a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
<Text style={[a.text_md, a.flex_shrink]} numberOfLines={1}>
|
|
||||||
{q}
|
|
||||||
</Text>
|
|
||||||
{filterCount > 0 ? (
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.flex_shrink_0,
|
a.flex_1,
|
||||||
a.rounded_sm,
|
a.flex_row,
|
||||||
a.px_sm,
|
a.align_center,
|
||||||
a.py_2xs,
|
a.gap_sm,
|
||||||
t.atoms.bg_contrast_25,
|
a.px_lg,
|
||||||
|
a.py_md,
|
||||||
|
a.pr_5xl,
|
||||||
|
(hovered || focused || pressed) && t.atoms.bg_contrast_25,
|
||||||
]}>
|
]}>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_xs, a.font_medium, t.atoms.text_contrast_medium]}>
|
emoji
|
||||||
<Plural value={filterCount} one="+# filter" other="+# filters" />
|
style={[a.text_md, a.leading_snug, a.flex_shrink]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{q}
|
||||||
</Text>
|
</Text>
|
||||||
|
{filterCount > 0 ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_shrink_0,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.px_sm,
|
||||||
|
a.py_2xs,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.font_medium,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Plural
|
||||||
|
value={filterCount}
|
||||||
|
one="+# filter"
|
||||||
|
other="+# filters"
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
)}
|
||||||
</Pressable>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
label={l`Remove ${q}`}
|
label={l`Remove ${q}`}
|
||||||
onPress={onRemove}
|
onPress={onRemove}
|
||||||
size="small"
|
size="small"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
shape="round">
|
shape="round"
|
||||||
|
style={[a.absolute, {right: 16}, a.bg_transparent]}>
|
||||||
<ButtonIcon icon={XIcon} />
|
<ButtonIcon icon={XIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user