add profiles to search history (#4169)

* add profiles to search history

* increasing horizontal padding slightly

* tightening up styling

* fixing navigation issue

* making corrections

* Make the search history profiles a little smaller

* bug stomping

* Fix issues

* Persist taps

* Rm unnecessary

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
zio/stable
Ryan Skinner 2024-06-04 11:31:24 -04:00 committed by GitHub
parent 6f1589971c
commit e7968bc8d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 195 additions and 7 deletions

View File

@ -1,8 +1,11 @@
import React from 'react'
import {
ActivityIndicator,
Image,
ImageStyle,
Platform,
Pressable,
StyleProp,
StyleSheet,
TextInput,
View,
@ -18,9 +21,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics'
import {createHitslop} from '#/lib/constants'
import {HITSLOP_10} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette'
import {MagnifyingGlassIcon} from '#/lib/icons'
import {makeProfileLink} from '#/lib/routes/links'
import {NavigationProp} from '#/lib/routes/types'
import {augmentSearchQuery} from '#/lib/strings/helpers'
import {s} from '#/lib/styles'
@ -46,6 +51,7 @@ import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
import {Post} from '#/view/com/post/Post'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {Link} from '#/view/com/util/Link'
import {List} from '#/view/com/util/List'
import {Text} from '#/view/com/util/text/Text'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
@ -488,6 +494,9 @@ export function SearchScreen(
const [showAutocomplete, setShowAutocomplete] = React.useState(false)
const [searchHistory, setSearchHistory] = React.useState<string[]>([])
const [selectedProfiles, setSelectedProfiles] = React.useState<
AppBskyActorDefs.ProfileViewBasic[]
>([])
useFocusEffect(
useNonReactiveCallback(() => {
@ -504,6 +513,10 @@ export function SearchScreen(
if (history !== null) {
setSearchHistory(JSON.parse(history))
}
const profiles = await AsyncStorage.getItem('selectedProfiles')
if (profiles !== null) {
setSelectedProfiles(JSON.parse(profiles))
}
} catch (e: any) {
logger.error('Failed to load search history', {message: e})
}
@ -562,6 +575,30 @@ export function SearchScreen(
[searchHistory, setSearchHistory],
)
const updateSelectedProfiles = React.useCallback(
async (profile: AppBskyActorDefs.ProfileViewBasic) => {
let newProfiles = [
profile,
...selectedProfiles.filter(p => p.did !== profile.did),
]
if (newProfiles.length > 5) {
newProfiles = newProfiles.slice(0, 5)
}
setSelectedProfiles(newProfiles)
try {
await AsyncStorage.setItem(
'selectedProfiles',
JSON.stringify(newProfiles),
)
} catch (e: any) {
logger.error('Failed to save selected profiles', {message: e})
}
},
[selectedProfiles, setSelectedProfiles],
)
const navigateToItem = React.useCallback(
(item: string) => {
scrollToTopWeb()
@ -598,6 +635,16 @@ export function SearchScreen(
[navigateToItem],
)
const handleProfileClick = React.useCallback(
(profile: AppBskyActorDefs.ProfileViewBasic) => {
// Slight delay to avoid updating during push nav animation.
setTimeout(() => {
updateSelectedProfiles(profile)
}, 400)
},
[updateSelectedProfiles],
)
const onSoftReset = React.useCallback(() => {
if (isWeb) {
// Empty params resets the URL to be /search rather than /search?q=
@ -629,6 +676,22 @@ export function SearchScreen(
[searchHistory],
)
const handleRemoveProfile = React.useCallback(
(profileToRemove: AppBskyActorDefs.ProfileViewBasic) => {
const updatedProfiles = selectedProfiles.filter(
profile => profile.did !== profileToRemove.did,
)
setSelectedProfiles(updatedProfiles)
AsyncStorage.setItem(
'selectedProfiles',
JSON.stringify(updatedProfiles),
).catch(e => {
logger.error('Failed to update selected profiles', {message: e})
})
},
[selectedProfiles],
)
return (
<View style={isWeb ? null : {flex: 1}}>
<CenteredView
@ -689,12 +752,16 @@ export function SearchScreen(
searchText={searchText}
onSubmit={onSubmit}
onResultPress={onAutocompleteResultPress}
onProfileClick={handleProfileClick}
/>
) : (
<SearchHistory
searchHistory={searchHistory}
selectedProfiles={selectedProfiles}
onItemClick={handleHistoryItemClick}
onProfileClick={handleProfileClick}
onRemoveItemClick={handleRemoveHistoryItem}
onRemoveProfileClick={handleRemoveProfile}
/>
)}
</View>
@ -814,12 +881,14 @@ let AutocompleteResults = ({
searchText,
onSubmit,
onResultPress,
onProfileClick,
}: {
isAutocompleteFetching: boolean
autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined
searchText: string
onSubmit: () => void
onResultPress: () => void
onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
}): React.ReactNode => {
const moderationOpts = useModerationOpts()
const {_} = useLingui()
@ -850,7 +919,10 @@ let AutocompleteResults = ({
key={item.did}
profile={item}
moderation={moderateProfile(item, moderationOpts)}
onPress={onResultPress}
onPress={() => {
onProfileClick(item)
onResultPress()
}}
/>
))}
<View style={{height: 200}} />
@ -861,17 +933,31 @@ let AutocompleteResults = ({
}
AutocompleteResults = React.memo(AutocompleteResults)
function truncateText(text: string, maxLength: number) {
if (text.length > maxLength) {
return text.substring(0, maxLength) + '...'
}
return text
}
function SearchHistory({
searchHistory,
selectedProfiles,
onItemClick,
onProfileClick,
onRemoveItemClick,
onRemoveProfileClick,
}: {
searchHistory: string[]
selectedProfiles: AppBskyActorDefs.ProfileViewBasic[]
onItemClick: (item: string) => void
onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
onRemoveItemClick: (item: string) => void
onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void
}) {
const {isTabletOrDesktop} = useWebMediaQueries()
const {isTabletOrDesktop, isMobile} = useWebMediaQueries()
const pal = usePalette('default')
return (
<CenteredView
sideBorders={isTabletOrDesktop}
@ -880,12 +966,68 @@ function SearchHistory({
height: isWeb ? '100vh' : undefined,
}}>
<View style={styles.searchHistoryContainer}>
{(searchHistory.length > 0 || selectedProfiles.length > 0) && (
<Text style={[pal.text, styles.searchHistoryTitle]}>
<Trans>Recent Searches</Trans>
</Text>
)}
{selectedProfiles.length > 0 && (
<View
style={[
styles.selectedProfilesContainer,
isMobile && styles.selectedProfilesContainerMobile,
]}>
<ScrollView
keyboardShouldPersistTaps="handled"
horizontal={true}
style={styles.profilesRow}
contentContainerStyle={{
borderWidth: 0,
}}>
{selectedProfiles.slice(0, 5).map((profile, index) => (
<View
key={index}
style={[
styles.profileItem,
isMobile && styles.profileItemMobile,
]}>
<Link
href={makeProfileLink(profile)}
title={profile.handle}
asAnchor
anchorNoUnderline
onBeforePress={() => onProfileClick(profile)}
style={styles.profilePressable}>
<Image
source={{uri: profile.avatar}}
style={styles.profileAvatar as StyleProp<ImageStyle>}
accessibilityIgnoresInvertColors
/>
<Text style={[pal.text, styles.profileName]}>
{truncateText(profile.displayName || '', 12)}
</Text>
</Link>
<Pressable
accessibilityRole="button"
accessibilityLabel="Remove profile"
accessibilityHint="Remove profile from search history"
onPress={() => onRemoveProfileClick(profile)}
hitSlop={createHitslop(6)}
style={styles.profileRemoveBtn}>
<FontAwesomeIcon
icon="xmark"
size={14}
style={pal.textLight as FontAwesomeIconStyle}
/>
</Pressable>
</View>
))}
</ScrollView>
</View>
)}
{searchHistory.length > 0 && (
<View style={styles.searchHistoryContent}>
<Text style={[pal.text, styles.searchHistoryTitle]}>
<Trans>Recent Searches</Trans>
</Text>
{searchHistory.map((historyItem, index) => (
{searchHistory.slice(0, 5).map((historyItem, index) => (
<View
key={index}
style={[
@ -982,11 +1124,57 @@ const styles = StyleSheet.create({
width: '100%',
paddingHorizontal: 12,
},
selectedProfilesContainer: {
marginTop: 10,
paddingHorizontal: 12,
height: 80,
},
selectedProfilesContainerMobile: {
height: 100,
},
profilesRow: {
flexDirection: 'row',
flexWrap: 'nowrap',
},
profileItem: {
alignItems: 'center',
marginRight: 15,
width: 78,
},
profileItemMobile: {
width: 70,
},
profilePressable: {
alignItems: 'center',
},
profileAvatar: {
width: 60,
height: 60,
borderRadius: 45,
},
profileName: {
fontSize: 12,
textAlign: 'center',
marginTop: 5,
},
profileRemoveBtn: {
position: 'absolute',
top: 0,
right: 5,
backgroundColor: 'white',
borderRadius: 10,
width: 18,
height: 18,
alignItems: 'center',
justifyContent: 'center',
},
searchHistoryContent: {
padding: 10,
paddingHorizontal: 10,
borderRadius: 8,
},
searchHistoryTitle: {
fontWeight: 'bold',
paddingVertical: 12,
paddingHorizontal: 10,
},
})