diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index b6680176bf..171b751d05 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -1,8 +1,11 @@ import React from 'react' import { ActivityIndicator, + Image, + ImageStyle, Platform, Pressable, + StyleProp, StyleSheet, TextInput, View, @@ -488,6 +491,9 @@ export function SearchScreen( const [showAutocomplete, setShowAutocomplete] = React.useState(false) const [searchHistory, setSearchHistory] = React.useState([]) + const [selectedProfiles, setSelectedProfiles] = React.useState< + AppBskyActorDefs.ProfileViewBasic[] + >([]) useFocusEffect( useNonReactiveCallback(() => { @@ -504,6 +510,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 +572,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() @@ -578,6 +612,28 @@ export function SearchScreen( [updateSearchHistory, navigation], ) + const navigateToProfile = React.useCallback( + (profile: AppBskyActorDefs.ProfileViewBasic) => { + scrollToTopWeb() + setShowAutocomplete(false) + updateSelectedProfiles(profile) + + if (isWeb) { + navigation.reset({ + index: 1, + routes: [ + {name: 'Search', params: {q: searchText}}, + {name: 'Profile', params: {name: profile.handle}}, + ], + }) + } else { + textInput.current?.blur() + navigation.navigate('Profile', {name: profile.handle}) + } + }, + [updateSelectedProfiles, navigation, searchText], + ) + const onSubmit = React.useCallback(() => { navigateToItem(searchText) }, [navigateToItem, searchText]) @@ -598,6 +654,13 @@ export function SearchScreen( [navigateToItem], ) + const handleProfileClick = React.useCallback( + (profile: AppBskyActorDefs.ProfileViewBasic) => { + navigateToProfile(profile) + }, + [navigateToProfile], + ) + const onSoftReset = React.useCallback(() => { if (isWeb) { // Empty params resets the URL to be /search rather than /search?q= @@ -629,6 +692,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 ( ) : ( )} @@ -814,12 +897,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 +935,10 @@ let AutocompleteResults = ({ key={item.did} profile={item} moderation={moderateProfile(item, moderationOpts)} - onPress={onResultPress} + onPress={() => { + onProfileClick(item) + onResultPress() + }} /> ))} @@ -861,17 +949,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 ( + {(searchHistory.length > 0 || selectedProfiles.length > 0) && ( + + Recent Searches + + )} + {selectedProfiles.length > 0 && ( + + + {selectedProfiles.slice(0, 5).map((profile, index) => ( + + onProfileClick(profile)} + hitSlop={HITSLOP_10} + style={styles.profilePressable}> + } + accessibilityIgnoresInvertColors + /> + + {truncateText(profile.displayName || '', 12)} + + + @{truncateText(profile.handle, 12)} + + + onRemoveProfileClick(profile)} + hitSlop={HITSLOP_10} + style={styles.profileRemoveBtn}> + + + + ))} + + + )} {searchHistory.length > 0 && ( - - Recent Searches - - {searchHistory.map((historyItem, index) => ( + {searchHistory.slice(0, 5).map((historyItem, index) => (