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>
This commit is contained in:
		
							parent
							
								
									6f1589971c
								
							
						
					
					
						commit
						e7968bc8d7
					
				
					 1 changed files with 195 additions and 7 deletions
				
			
		|  | @ -1,8 +1,11 @@ | ||||||
| import React from 'react' | import React from 'react' | ||||||
| import { | import { | ||||||
|   ActivityIndicator, |   ActivityIndicator, | ||||||
|  |   Image, | ||||||
|  |   ImageStyle, | ||||||
|   Platform, |   Platform, | ||||||
|   Pressable, |   Pressable, | ||||||
|  |   StyleProp, | ||||||
|   StyleSheet, |   StyleSheet, | ||||||
|   TextInput, |   TextInput, | ||||||
|   View, |   View, | ||||||
|  | @ -18,9 +21,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage' | ||||||
| import {useFocusEffect, useNavigation} from '@react-navigation/native' | import {useFocusEffect, useNavigation} from '@react-navigation/native' | ||||||
| 
 | 
 | ||||||
| import {useAnalytics} from '#/lib/analytics/analytics' | import {useAnalytics} from '#/lib/analytics/analytics' | ||||||
|  | import {createHitslop} from '#/lib/constants' | ||||||
| import {HITSLOP_10} from '#/lib/constants' | import {HITSLOP_10} from '#/lib/constants' | ||||||
| import {usePalette} from '#/lib/hooks/usePalette' | import {usePalette} from '#/lib/hooks/usePalette' | ||||||
| import {MagnifyingGlassIcon} from '#/lib/icons' | import {MagnifyingGlassIcon} from '#/lib/icons' | ||||||
|  | import {makeProfileLink} from '#/lib/routes/links' | ||||||
| import {NavigationProp} from '#/lib/routes/types' | import {NavigationProp} from '#/lib/routes/types' | ||||||
| import {augmentSearchQuery} from '#/lib/strings/helpers' | import {augmentSearchQuery} from '#/lib/strings/helpers' | ||||||
| import {s} from '#/lib/styles' | import {s} from '#/lib/styles' | ||||||
|  | @ -46,6 +51,7 @@ import {Pager} from '#/view/com/pager/Pager' | ||||||
| import {TabBar} from '#/view/com/pager/TabBar' | import {TabBar} from '#/view/com/pager/TabBar' | ||||||
| import {Post} from '#/view/com/post/Post' | import {Post} from '#/view/com/post/Post' | ||||||
| import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' | import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' | ||||||
|  | import {Link} from '#/view/com/util/Link' | ||||||
| import {List} from '#/view/com/util/List' | import {List} from '#/view/com/util/List' | ||||||
| import {Text} from '#/view/com/util/text/Text' | import {Text} from '#/view/com/util/text/Text' | ||||||
| import {CenteredView, ScrollView} from '#/view/com/util/Views' | import {CenteredView, ScrollView} from '#/view/com/util/Views' | ||||||
|  | @ -488,6 +494,9 @@ export function SearchScreen( | ||||||
| 
 | 
 | ||||||
|   const [showAutocomplete, setShowAutocomplete] = React.useState(false) |   const [showAutocomplete, setShowAutocomplete] = React.useState(false) | ||||||
|   const [searchHistory, setSearchHistory] = React.useState<string[]>([]) |   const [searchHistory, setSearchHistory] = React.useState<string[]>([]) | ||||||
|  |   const [selectedProfiles, setSelectedProfiles] = React.useState< | ||||||
|  |     AppBskyActorDefs.ProfileViewBasic[] | ||||||
|  |   >([]) | ||||||
| 
 | 
 | ||||||
|   useFocusEffect( |   useFocusEffect( | ||||||
|     useNonReactiveCallback(() => { |     useNonReactiveCallback(() => { | ||||||
|  | @ -504,6 +513,10 @@ export function SearchScreen( | ||||||
|         if (history !== null) { |         if (history !== null) { | ||||||
|           setSearchHistory(JSON.parse(history)) |           setSearchHistory(JSON.parse(history)) | ||||||
|         } |         } | ||||||
|  |         const profiles = await AsyncStorage.getItem('selectedProfiles') | ||||||
|  |         if (profiles !== null) { | ||||||
|  |           setSelectedProfiles(JSON.parse(profiles)) | ||||||
|  |         } | ||||||
|       } catch (e: any) { |       } catch (e: any) { | ||||||
|         logger.error('Failed to load search history', {message: e}) |         logger.error('Failed to load search history', {message: e}) | ||||||
|       } |       } | ||||||
|  | @ -562,6 +575,30 @@ export function SearchScreen( | ||||||
|     [searchHistory, setSearchHistory], |     [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( |   const navigateToItem = React.useCallback( | ||||||
|     (item: string) => { |     (item: string) => { | ||||||
|       scrollToTopWeb() |       scrollToTopWeb() | ||||||
|  | @ -598,6 +635,16 @@ export function SearchScreen( | ||||||
|     [navigateToItem], |     [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(() => { |   const onSoftReset = React.useCallback(() => { | ||||||
|     if (isWeb) { |     if (isWeb) { | ||||||
|       // Empty params resets the URL to be /search rather than /search?q=
 |       // Empty params resets the URL to be /search rather than /search?q=
 | ||||||
|  | @ -629,6 +676,22 @@ export function SearchScreen( | ||||||
|     [searchHistory], |     [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 ( |   return ( | ||||||
|     <View style={isWeb ? null : {flex: 1}}> |     <View style={isWeb ? null : {flex: 1}}> | ||||||
|       <CenteredView |       <CenteredView | ||||||
|  | @ -689,12 +752,16 @@ export function SearchScreen( | ||||||
|             searchText={searchText} |             searchText={searchText} | ||||||
|             onSubmit={onSubmit} |             onSubmit={onSubmit} | ||||||
|             onResultPress={onAutocompleteResultPress} |             onResultPress={onAutocompleteResultPress} | ||||||
|  |             onProfileClick={handleProfileClick} | ||||||
|           /> |           /> | ||||||
|         ) : ( |         ) : ( | ||||||
|           <SearchHistory |           <SearchHistory | ||||||
|             searchHistory={searchHistory} |             searchHistory={searchHistory} | ||||||
|  |             selectedProfiles={selectedProfiles} | ||||||
|             onItemClick={handleHistoryItemClick} |             onItemClick={handleHistoryItemClick} | ||||||
|  |             onProfileClick={handleProfileClick} | ||||||
|             onRemoveItemClick={handleRemoveHistoryItem} |             onRemoveItemClick={handleRemoveHistoryItem} | ||||||
|  |             onRemoveProfileClick={handleRemoveProfile} | ||||||
|           /> |           /> | ||||||
|         )} |         )} | ||||||
|       </View> |       </View> | ||||||
|  | @ -814,12 +881,14 @@ let AutocompleteResults = ({ | ||||||
|   searchText, |   searchText, | ||||||
|   onSubmit, |   onSubmit, | ||||||
|   onResultPress, |   onResultPress, | ||||||
|  |   onProfileClick, | ||||||
| }: { | }: { | ||||||
|   isAutocompleteFetching: boolean |   isAutocompleteFetching: boolean | ||||||
|   autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined |   autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined | ||||||
|   searchText: string |   searchText: string | ||||||
|   onSubmit: () => void |   onSubmit: () => void | ||||||
|   onResultPress: () => void |   onResultPress: () => void | ||||||
|  |   onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void | ||||||
| }): React.ReactNode => { | }): React.ReactNode => { | ||||||
|   const moderationOpts = useModerationOpts() |   const moderationOpts = useModerationOpts() | ||||||
|   const {_} = useLingui() |   const {_} = useLingui() | ||||||
|  | @ -850,7 +919,10 @@ let AutocompleteResults = ({ | ||||||
|               key={item.did} |               key={item.did} | ||||||
|               profile={item} |               profile={item} | ||||||
|               moderation={moderateProfile(item, moderationOpts)} |               moderation={moderateProfile(item, moderationOpts)} | ||||||
|               onPress={onResultPress} |               onPress={() => { | ||||||
|  |                 onProfileClick(item) | ||||||
|  |                 onResultPress() | ||||||
|  |               }} | ||||||
|             /> |             /> | ||||||
|           ))} |           ))} | ||||||
|           <View style={{height: 200}} /> |           <View style={{height: 200}} /> | ||||||
|  | @ -861,17 +933,31 @@ let AutocompleteResults = ({ | ||||||
| } | } | ||||||
| AutocompleteResults = React.memo(AutocompleteResults) | AutocompleteResults = React.memo(AutocompleteResults) | ||||||
| 
 | 
 | ||||||
|  | function truncateText(text: string, maxLength: number) { | ||||||
|  |   if (text.length > maxLength) { | ||||||
|  |     return text.substring(0, maxLength) + '...' | ||||||
|  |   } | ||||||
|  |   return text | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function SearchHistory({ | function SearchHistory({ | ||||||
|   searchHistory, |   searchHistory, | ||||||
|  |   selectedProfiles, | ||||||
|   onItemClick, |   onItemClick, | ||||||
|  |   onProfileClick, | ||||||
|   onRemoveItemClick, |   onRemoveItemClick, | ||||||
|  |   onRemoveProfileClick, | ||||||
| }: { | }: { | ||||||
|   searchHistory: string[] |   searchHistory: string[] | ||||||
|  |   selectedProfiles: AppBskyActorDefs.ProfileViewBasic[] | ||||||
|   onItemClick: (item: string) => void |   onItemClick: (item: string) => void | ||||||
|  |   onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void | ||||||
|   onRemoveItemClick: (item: string) => void |   onRemoveItemClick: (item: string) => void | ||||||
|  |   onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void | ||||||
| }) { | }) { | ||||||
|   const {isTabletOrDesktop} = useWebMediaQueries() |   const {isTabletOrDesktop, isMobile} = useWebMediaQueries() | ||||||
|   const pal = usePalette('default') |   const pal = usePalette('default') | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <CenteredView |     <CenteredView | ||||||
|       sideBorders={isTabletOrDesktop} |       sideBorders={isTabletOrDesktop} | ||||||
|  | @ -880,12 +966,68 @@ function SearchHistory({ | ||||||
|         height: isWeb ? '100vh' : undefined, |         height: isWeb ? '100vh' : undefined, | ||||||
|       }}> |       }}> | ||||||
|       <View style={styles.searchHistoryContainer}> |       <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 && ( |         {searchHistory.length > 0 && ( | ||||||
|           <View style={styles.searchHistoryContent}> |           <View style={styles.searchHistoryContent}> | ||||||
|             <Text style={[pal.text, styles.searchHistoryTitle]}> |             {searchHistory.slice(0, 5).map((historyItem, index) => ( | ||||||
|               <Trans>Recent Searches</Trans> |  | ||||||
|             </Text> |  | ||||||
|             {searchHistory.map((historyItem, index) => ( |  | ||||||
|               <View |               <View | ||||||
|                 key={index} |                 key={index} | ||||||
|                 style={[ |                 style={[ | ||||||
|  | @ -982,11 +1124,57 @@ const styles = StyleSheet.create({ | ||||||
|     width: '100%', |     width: '100%', | ||||||
|     paddingHorizontal: 12, |     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: { |   searchHistoryContent: { | ||||||
|     padding: 10, |     paddingHorizontal: 10, | ||||||
|     borderRadius: 8, |     borderRadius: 8, | ||||||
|   }, |   }, | ||||||
|   searchHistoryTitle: { |   searchHistoryTitle: { | ||||||
|     fontWeight: 'bold', |     fontWeight: 'bold', | ||||||
|  |     paddingVertical: 12, | ||||||
|  |     paddingHorizontal: 10, | ||||||
|   }, |   }, | ||||||
| }) | }) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue