diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index b6680176..003f9a8b 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, @@ -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([]) + 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 ( ) : ( )} @@ -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() + }} /> ))} @@ -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 ( + {(searchHistory.length > 0 || selectedProfiles.length > 0) && ( + + Recent Searches + + )} + {selectedProfiles.length > 0 && ( + + + {selectedProfiles.slice(0, 5).map((profile, index) => ( + + onProfileClick(profile)} + style={styles.profilePressable}> + } + accessibilityIgnoresInvertColors + /> + + {truncateText(profile.displayName || '', 12)} + + + onRemoveProfileClick(profile)} + hitSlop={createHitslop(6)} + style={styles.profileRemoveBtn}> + + + + ))} + + + )} {searchHistory.length > 0 && ( - - Recent Searches - - {searchHistory.map((historyItem, index) => ( + {searchHistory.slice(0, 5).map((historyItem, index) => (