From 5b04f118aeb7e995432308acb2bb7b4dc6d305b4 Mon Sep 17 00:00:00 2001 From: Mikael Brevik Date: Fri, 23 Oct 2020 11:03:31 +0200 Subject: [PATCH] feat: Adds ability to search for favorites. fixes #422 (#590) --- src/favorites/index.tsx | 3 +- src/location-search/LocationResults.tsx | 78 +++++++++++++++++------- src/location-search/LocationSearch.tsx | 80 ++++++++++++++++++------- src/location-search/types.ts | 6 ++ 4 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 src/location-search/types.ts diff --git a/src/favorites/index.tsx b/src/favorites/index.tsx index 15367fff26..6c456924ce 100644 --- a/src/favorites/index.tsx +++ b/src/favorites/index.tsx @@ -1,10 +1,9 @@ -import {LocationFavorite} from './types'; import React from 'react'; import {Text} from 'react-native'; import {MapPointPin} from '../assets/svg/icons/places'; export type FavoriteIconProps = { - favorite?: LocationFavorite; + favorite?: {emoji?: string}; }; export function FavoriteIcon({favorite}: FavoriteIconProps) { diff --git a/src/location-search/LocationResults.tsx b/src/location-search/LocationResults.tsx index 71c92344a7..72275cd573 100644 --- a/src/location-search/LocationResults.tsx +++ b/src/location-search/LocationResults.tsx @@ -1,15 +1,16 @@ import React from 'react'; import {View, Text, TouchableOpacity} from 'react-native'; import {StyleSheet} from '../theme'; -import {Location} from '../favorites/types'; import LocationIcon from '../components/location-icon'; import insets from '../utils/insets'; import {ArrowUpLeft} from '../assets/svg/icons/navigation'; +import {LocationSearchResult} from './types'; +import {FavoriteIcon} from '../favorites'; type Props = { - title: string; - locations: Location[]; - onSelect: (location: Location) => void; + title?: string; + locations: LocationSearchResult[]; + onSelect: (location: LocationSearchResult) => void; onPrefillText: (text: string) => void; }; @@ -22,40 +23,48 @@ const LocationResults: React.FC = ({ const styles = useThemeStyles(); return ( <> - - {title} - - + {title && ( + + {title} + + + )} - {locations.map((location) => ( - + {locations.map(mapToVisibleSearchResult).map((searchResult) => ( + onSelect(location)} + onPress={() => onSelect(searchResult.selectable)} style={styles.locationButton} > - + {searchResult.emoji ? ( + + ) : ( + + )} - {location.name} - {location.locality} + {searchResult.text} + {searchResult.subtext} onPrefillText(location.name + ' ')} + onPress={() => onPrefillText(searchResult.prefill + ' ')} > @@ -66,6 +75,35 @@ const LocationResults: React.FC = ({ ); }; +function mapToVisibleSearchResult(searchResult: LocationSearchResult) { + const location = searchResult.location; + if (!searchResult.favoriteInfo) { + return { + key: location.id, + selectable: searchResult, + location, + text: location.name, + subtext: location.locality, + prefill: location.name, + }; + } + + const text = searchResult.favoriteInfo.name ?? location.name; + const subtext = searchResult.favoriteInfo.name + ? `${location.name}, ${location.locality}` + : location.locality; + + return { + key: searchResult.favoriteInfo.id, + selectable: searchResult, + location, + text, + subtext, + prefill: location.name, + emoji: searchResult.favoriteInfo.emoji, + }; +} + export default LocationResults; const useThemeStyles = StyleSheet.createThemeHook((theme) => ({ diff --git a/src/location-search/LocationSearch.tsx b/src/location-search/LocationSearch.tsx index 0435b47b4b..f8b0a5e5f3 100644 --- a/src/location-search/LocationSearch.tsx +++ b/src/location-search/LocationSearch.tsx @@ -4,7 +4,11 @@ import {Text, TextInput, View, Keyboard} from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; import Header from '../ScreenHeader'; import Input from '../components/input'; -import {Location, LocationWithMetadata} from '../favorites/types'; +import { + Location, + LocationWithMetadata, + UserFavorites, +} from '../favorites/types'; import {useGeolocationState} from '../GeolocationContext'; import {RootStackParamList} from '../navigation'; import {useSearchHistory} from '../search-history'; @@ -20,6 +24,8 @@ import {SafeAreaView} from 'react-native-safe-area-context'; import {useGeocoder} from '../geocoder'; import MessageBox from '../message-box'; import {ErrorType} from '../api/utils'; +import {useFavorites} from '../favorites'; +import {LocationSearchResult} from './types'; export type Props = { navigation: LocationSearchNavigationProp; @@ -48,16 +54,18 @@ const LocationSearch: React.FC = ({ }) => { const styles = useThemeStyles(); const {history, addSearchEntry} = useSearchHistory(); + const {favorites} = useFavorites(); const [text, setText] = useState(initialLocation?.name ?? ''); const debouncedText = useDebounce(text, 200); - const previousLocations = filterPreviousLocations(debouncedText, history); + const previousLocations = filterPreviousLocations( + debouncedText, + history, + favorites, + ); - const { - location: geolocation, - requestPermission: requestGeoPermission, - } = useGeolocationState(); + const {location: geolocation} = useGeolocationState(); const {locations, error} = useGeocoder(debouncedText, geolocation?.coords ?? null) ?? []; @@ -90,8 +98,16 @@ const LocationSearch: React.FC = ({ }); }; - const onSearchSelect = (location: Location) => - onSelect({...location, resultType: 'search'}); + const onSearchSelect = (searchResult: LocationSearchResult) => { + if (!searchResult.favoriteInfo) { + return onSelect({...searchResult.location, resultType: 'search'}); + } + return onSelect({ + ...searchResult.location, + resultType: 'favorite', + favoriteId: searchResult.favoriteInfo.id, + }); + }; const inputRef = useRef(null); @@ -172,7 +188,6 @@ const LocationSearch: React.FC = ({ > {hasPreviousResults && ( - searchText - ? previousLocations.filter((l) => - l.name?.toLowerCase()?.startsWith(searchText.toLowerCase()), - ) - : previousLocations; + favorites?: UserFavorites, +): LocationSearchResult[] => { + const mappedHistory: LocationSearchResult[] = + previousLocations?.map((location) => ({ + location, + })) ?? []; + + if (!searchText) { + return mappedHistory; + } + + const matchText = (text?: string) => + text?.toLowerCase()?.startsWith(searchText.toLowerCase()); + const filteredFavorites: LocationSearchResult[] = (favorites ?? []) + .filter( + (favorite) => + matchText(favorite.location?.name) || matchText(favorite.name), + ) + .map(({location, ...favoriteInfo}) => ({ + location, + favoriteInfo, + })); + + return filteredFavorites.concat( + mappedHistory.filter((l) => matchText(l.location.name)), + ); +}; const filterCurrentLocation = ( locations: Location[] | null, - previousLocations: Location[] | null, -): Location[] => { - if (!previousLocations?.length) return locations ?? []; - if (!locations) return []; - return locations.filter( - (l) => !previousLocations.some((pl) => pl.id === l.id), - ); + previousLocations: LocationSearchResult[] | null, +): LocationSearchResult[] => { + if (!previousLocations?.length || !locations) + return locations?.map((location) => ({location})) ?? []; + return locations + .filter((l) => !previousLocations.some((pl) => pl.location.id === l.id)) + .map((location) => ({location})); }; const useThemeStyles = StyleSheet.createThemeHook((theme) => ({ diff --git a/src/location-search/types.ts b/src/location-search/types.ts new file mode 100644 index 0000000000..9cd99bae08 --- /dev/null +++ b/src/location-search/types.ts @@ -0,0 +1,6 @@ +import {Location, LocationFavorite} from '../favorites/types'; + +export type LocationSearchResult = { + location: Location; + favoriteInfo?: Omit; +};