refactor(gif): remove text autocomplete, increase search debounce

Remove the inline text suggestion list from the GIF picker. The
Threads-style approach of just letting the GIF search results update
directly is simpler and gets users to the GIFs faster.

Increase the search throttle from 500ms to 750ms to reduce churn
while typing and give results time to load before the next query fires.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vineyardbovines
2026-04-14 15:27:34 -04:00
parent 054a6878c5
commit 924f3e3cbc
2 changed files with 42 additions and 76 deletions
+1 -13
View File
@@ -10,7 +10,6 @@ import {GifPickerErrorBoundary} from '#/features/gifPicker/components/GifPickerE
import {GifPickerGrid} from '#/features/gifPicker/components/GifPickerGrid'
import {GifPickerHeader} from '#/features/gifPicker/components/GifPickerHeader'
import {GifPickerPlaceholder} from '#/features/gifPicker/components/GifPickerPlaceholder'
import {useGifAutocomplete} from '#/features/gifPicker/hooks/useGifAutocomplete'
import {useGifPickerData} from '#/features/gifPicker/hooks/useGifPickerData'
import {type Gif} from '#/features/gifPicker/types'
@@ -65,16 +64,7 @@ function GifPickerBody({
const textInputRef = useRef<TextInput>(null)
const listRef = useRef<ListMethods>(null)
const [rawSearch, setRawSearch] = useState('')
const search = useThrottledValue(rawSearch, 500)
const autocomplete = useGifAutocomplete({
onSelectSuggestion: text => {
setRawSearch(text)
// Set the TextInput's displayed value to match
textInputRef.current?.setNativeProps({text})
listRef.current?.scrollToOffset({offset: 0, animated: false})
},
})
const search = useThrottledValue(rawSearch, 750)
const {
data,
@@ -107,7 +97,6 @@ function GifPickerBody({
const onChangeSearch = (text: string) => {
setRawSearch(text)
autocomplete.handleTextChange(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}
@@ -118,7 +107,6 @@ function GifPickerBody({
onChangeText={onChangeSearch}
onClose={() => control.close()}
onEscape={() => control.close()}
autocomplete={autocomplete}
/>
{!hasData && (
<GifPickerPlaceholder
@@ -9,87 +9,65 @@ import * as TextField from '#/components/forms/TextField'
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
import {IS_WEB} from '#/env'
import {
GIF_AUTOCOMPLETE_LISTBOX_ID,
GifAutocompleteSuggestions,
suggestionItemId,
} from '#/features/gifPicker/components/GifAutocompleteSuggestions'
import {type GifAutocompleteState} from '#/features/gifPicker/hooks/useGifAutocomplete'
export function GifPickerHeader({
inputRef,
onChangeText,
onClose,
onEscape,
autocomplete,
}: {
inputRef: Ref<TextInput>
onChangeText: (text: string) => void
onClose: () => void
onEscape: () => void
autocomplete: GifAutocompleteState
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
return (
<View style={[native(a.pt_4xl), a.relative, a.mb_lg, a.pb_sm, t.atoms.bg]}>
<View style={[a.flex_row, a.align_center, !gtMobile && web(a.gap_md)]}>
{!gtMobile && IS_WEB && (
<Button
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={onClose}
label={_(msg`Close GIF dialog`)}>
<ButtonIcon icon={Arrow} size="md" />
</Button>
)}
<TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
<TextField.Icon icon={Search} />
<TextField.Input
label={_(msg`Search GIFs`)}
placeholder={_(msg`Search GIFs`)}
onChangeText={onChangeText}
returnKeyType="search"
clearButtonMode="while-editing"
inputRef={inputRef}
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
if (!autocomplete.handleKeyDown('Escape')) {
onEscape()
}
} else {
autocomplete.handleKeyDown(nativeEvent.key)
}
}}
// @ts-ignore web-only ARIA props
role={autocomplete.isVisible ? 'combobox' : undefined}
aria-controls={
autocomplete.isVisible ? GIF_AUTOCOMPLETE_LISTBOX_ID : undefined
}
aria-expanded={autocomplete.isVisible}
aria-autocomplete={autocomplete.isVisible ? 'list' : undefined}
aria-activedescendant={
autocomplete.isVisible && autocomplete.activeIndex >= 0
? suggestionItemId(autocomplete.activeIndex)
: undefined
}
/>
</TextField.Root>
</View>
{autocomplete.isVisible && (
<GifAutocompleteSuggestions
suggestions={autocomplete.suggestions}
activeIndex={autocomplete.activeIndex}
onSelect={autocomplete.selectSuggestion}
/>
<View
style={[
native(a.pt_4xl),
a.relative,
a.mb_lg,
a.flex_row,
a.align_center,
!gtMobile && web(a.gap_md),
a.pb_sm,
t.atoms.bg,
]}>
{!gtMobile && IS_WEB && (
<Button
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={onClose}
label={_(msg`Close GIF dialog`)}>
<ButtonIcon icon={Arrow} size="md" />
</Button>
)}
<TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
<TextField.Icon icon={Search} />
<TextField.Input
label={_(msg`Search GIFs`)}
placeholder={_(msg`Search GIFs`)}
onChangeText={onChangeText}
returnKeyType="search"
clearButtonMode="while-editing"
inputRef={inputRef}
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
onEscape()
}
}}
/>
</TextField.Root>
{/* future: tabs (Trending / Recents / Categories) render here */}
</View>
)
}