basic implementation of search history (#2597)
Co-authored-by: Ryan Skinner <ryanskinner@gmail.com>zio/stable
parent
abac959d03
commit
cda4fe4a7f
|
@ -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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue