basic implementation of search history (#2597)

Co-authored-by: Ryan Skinner <ryanskinner@gmail.com>
zio/stable
Paul Frazee 2024-01-23 12:06:40 -08:00 committed by GitHub
parent abac959d03
commit cda4fe4a7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 131 additions and 6 deletions

View File

@ -51,6 +51,7 @@ import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
import {isNative, isWeb} from '#/platform/detection' import {isNative, isWeb} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import AsyncStorage from '@react-native-async-storage/async-storage'
function Loader() { function Loader() {
const pal = usePalette('default') const pal = usePalette('default')
@ -464,11 +465,28 @@ export function SearchScreen(
const [inputIsFocused, setInputIsFocused] = React.useState(false) const [inputIsFocused, setInputIsFocused] = React.useState(false)
const [showAutocompleteResults, setShowAutocompleteResults] = const [showAutocompleteResults, setShowAutocompleteResults] =
React.useState(false) React.useState(false)
const [searchHistory, setSearchHistory] = React.useState<string[]>([])
React.useEffect(() => {
const loadSearchHistory = async () => {
try {
const history = await AsyncStorage.getItem('searchHistory')
if (history !== null) {
setSearchHistory(JSON.parse(history))
}
} catch (e: any) {
logger.error('Failed to load search history', e)
}
}
loadSearchHistory()
}, [])
const onPressMenu = React.useCallback(() => { const onPressMenu = React.useCallback(() => {
track('ViewHeader:MenuButtonClicked') track('ViewHeader:MenuButtonClicked')
setDrawerOpen(true) setDrawerOpen(true)
}, [track, setDrawerOpen]) }, [track, setDrawerOpen])
const onPressCancelSearch = React.useCallback(() => { const onPressCancelSearch = React.useCallback(() => {
scrollToTopWeb() scrollToTopWeb()
textInput.current?.blur() textInput.current?.blur()
@ -477,22 +495,26 @@ export function SearchScreen(
if (searchDebounceTimeout.current) if (searchDebounceTimeout.current)
clearTimeout(searchDebounceTimeout.current) clearTimeout(searchDebounceTimeout.current)
}, [textInput]) }, [textInput])
const onPressClearQuery = React.useCallback(() => { const onPressClearQuery = React.useCallback(() => {
scrollToTopWeb() scrollToTopWeb()
setQuery('') setQuery('')
setShowAutocompleteResults(false) setShowAutocompleteResults(false)
}, [setQuery]) }, [setQuery])
const onChangeText = React.useCallback( const onChangeText = React.useCallback(
async (text: string) => { async (text: string) => {
scrollToTopWeb() scrollToTopWeb()
setQuery(text) setQuery(text)
if (text.length > 0) { if (text.length > 0) {
setIsFetching(true) setIsFetching(true)
setShowAutocompleteResults(true) setShowAutocompleteResults(true)
if (searchDebounceTimeout.current) if (searchDebounceTimeout.current) {
clearTimeout(searchDebounceTimeout.current) clearTimeout(searchDebounceTimeout.current)
}
searchDebounceTimeout.current = setTimeout(async () => { searchDebounceTimeout.current = setTimeout(async () => {
const results = await search({query: text, limit: 30}) const results = await search({query: text, limit: 30})
@ -503,8 +525,9 @@ export function SearchScreen(
} }
}, 300) }, 300)
} else { } else {
if (searchDebounceTimeout.current) if (searchDebounceTimeout.current) {
clearTimeout(searchDebounceTimeout.current) clearTimeout(searchDebounceTimeout.current)
}
setSearchResults([]) setSearchResults([])
setIsFetching(false) setIsFetching(false)
setShowAutocompleteResults(false) setShowAutocompleteResults(false)
@ -512,10 +535,36 @@ export function SearchScreen(
}, },
[setQuery, search, setSearchResults], [setQuery, search, setSearchResults],
) )
const updateSearchHistory = React.useCallback(
async (newQuery: string) => {
newQuery = newQuery.trim()
if (newQuery && !searchHistory.includes(newQuery)) {
let newHistory = [newQuery, ...searchHistory]
if (newHistory.length > 5) {
newHistory = newHistory.slice(0, 5)
}
setSearchHistory(newHistory)
try {
await AsyncStorage.setItem(
'searchHistory',
JSON.stringify(newHistory),
)
} catch (e: any) {
logger.error('Failed to save search history', e)
}
}
},
[searchHistory, setSearchHistory],
)
const onSubmit = React.useCallback(() => { const onSubmit = React.useCallback(() => {
scrollToTopWeb() scrollToTopWeb()
setShowAutocompleteResults(false) setShowAutocompleteResults(false)
}, [setShowAutocompleteResults]) updateSearchHistory(query)
}, [query, setShowAutocompleteResults, updateSearchHistory])
const onSoftReset = React.useCallback(() => { const onSoftReset = React.useCallback(() => {
scrollToTopWeb() scrollToTopWeb()
@ -534,6 +583,21 @@ export function SearchScreen(
}, [onSoftReset, setMinimalShellMode]), }, [onSoftReset, setMinimalShellMode]),
) )
const handleHistoryItemClick = (item: React.SetStateAction<string>) => {
setQuery(item)
onSubmit()
}
const handleRemoveHistoryItem = (itemToRemove: string) => {
const updatedHistory = searchHistory.filter(item => item !== itemToRemove)
setSearchHistory(updatedHistory)
AsyncStorage.setItem('searchHistory', JSON.stringify(updatedHistory)).catch(
e => {
logger.error('Failed to update search history', e)
},
)
}
return ( return (
<View style={isWeb ? null : {flex: 1}}> <View style={isWeb ? null : {flex: 1}}>
<CenteredView <CenteredView
@ -581,7 +645,12 @@ export function SearchScreen(
style={[pal.text, styles.headerSearchInput]} style={[pal.text, styles.headerSearchInput]}
keyboardAppearance={theme.colorScheme} keyboardAppearance={theme.colorScheme}
onFocus={() => setInputIsFocused(true)} onFocus={() => setInputIsFocused(true)}
onBlur={() => setInputIsFocused(false)} onBlur={() => {
// HACK
// give 100ms to not stop click handlers in the search history
// -prf
setTimeout(() => setInputIsFocused(false), 100)
}}
onChangeText={onChangeText} onChangeText={onChangeText}
onSubmitEditing={onSubmit} onSubmitEditing={onSubmit}
autoFocus={false} autoFocus={false}
@ -623,9 +692,9 @@ export function SearchScreen(
) : undefined} ) : undefined}
</CenteredView> </CenteredView>
{showAutocompleteResults && moderationOpts ? ( {showAutocompleteResults ? (
<> <>
{isFetching ? ( {isFetching || !moderationOpts ? (
<Loader /> <Loader />
) : ( ) : (
<ScrollView <ScrollView
@ -664,6 +733,42 @@ export function SearchScreen(
</ScrollView> </ScrollView>
)} )}
</> </>
) : !query && inputIsFocused ? (
<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]}>
Recent Searches
</Text>
{searchHistory.map((historyItem, index) => (
<View key={index} style={styles.historyItemContainer}>
<Pressable
accessibilityRole="button"
onPress={() => handleHistoryItemClick(historyItem)}
style={styles.historyItem}>
<Text style={pal.text}>{historyItem}</Text>
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() => handleRemoveHistoryItem(historyItem)}>
<FontAwesomeIcon
icon="xmark"
size={16}
style={pal.textLight as FontAwesomeIconStyle}
/>
</Pressable>
</View>
))}
</View>
)}
</View>
</CenteredView>
) : ( ) : (
<SearchScreenInner query={query} /> <SearchScreenInner query={query} />
)} )}
@ -725,4 +830,24 @@ const styles = StyleSheet.create({
top: isWeb ? HEADER_HEIGHT : 0, top: isWeb ? HEADER_HEIGHT : 0,
zIndex: 1, zIndex: 1,
}, },
searchHistoryContainer: {
width: '100%',
paddingHorizontal: 12,
},
searchHistoryContent: {
padding: 10,
borderRadius: 8,
},
searchHistoryTitle: {
fontWeight: 'bold',
},
historyItem: {
paddingVertical: 8,
},
historyItemContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
},
}) })