diff --git a/README.md b/README.md index 5b97b3cb2..cff7933f1 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,6 @@ fastlane prod 1. Edit `package.json` and update the `version` per semantic versioning rules: bump the patch version if the only changes were bug fixes, bump minor version if there were new features, and bump the major version if the app was completely re-written or can't import data from previous versions. 1. `npm install` to set the version in `package-lock.json` 1. Commit changes -1. `bundle exec fastlane tag` to create a tag and bump the build number. You'll be prompted to enter those release notes you wrote. +1. `bundle exec fastlane tag` to create a tag and bump the build number. You'll be prompted to enter those release notes you wrote. (:wq to save and exit) 1. `bundle exec fastlane release` to build and push a release to Github 1. `bundle exec fastlane internal` to distribute the builds to TestFlight and the Play Store diff --git a/src/components/Explore/SearchScreens/ExploreLocationSearch.js b/src/components/Explore/SearchScreens/ExploreLocationSearch.js index 4007cac76..e345a67b2 100644 --- a/src/components/Explore/SearchScreens/ExploreLocationSearch.js +++ b/src/components/Explore/SearchScreens/ExploreLocationSearch.js @@ -1,9 +1,14 @@ // @flow import { useNavigation } from "@react-navigation/native"; +import { useQueryClient } from "@tanstack/react-query"; import fetchSearchResults from "api/search"; +import classnames from "classnames"; import { + ActivityIndicator, Body3, + Button, + Map, SearchBar, ViewWrapper } from "components/SharedComponents"; @@ -11,20 +16,142 @@ import { Pressable, View } from "components/styledComponents"; import type { Node } from "react"; import React, { useCallback, - useState + useReducer, + useRef } from "react"; -import { FlatList } from "react-native"; +import { Keyboard } from "react-native"; +import { useTheme } from "react-native-paper"; import { useAuthenticatedQuery } from "sharedHooks"; +import useTranslation from "sharedHooks/useTranslation"; +import { getShadowStyle } from "styles/global"; + +const DELTA = 0.02; + +const initialState = { + loading: true, + mapType: "standard", + locationName: null, + region: { + latitude: 0.0, + longitude: 0.0, + latitudeDelta: DELTA, + longitudeDelta: DELTA + }, + hidePlaceResults: true, + place: null +}; + +const reducer = ( state, action ) => { + switch ( action.type ) { + case "SET_LOADING": + return { + ...state, + loading: action.loading + }; + case "SET_MAP_TYPE": + return { + ...state, + mapType: action.mapType + }; + case "UPDATE_REGION": + return { + ...state, + region: action.region + }; + + case "SELECT_PLACE_RESULT": + return { + ...state, + locationName: action.locationName, + region: action.region, + hidePlaceResults: true, + place: action.place + }; + case "UPDATE_LOCATION_NAME": + return { + ...state, + locationName: action.locationName, + hidePlaceResults: false + }; + default: + throw new Error(); + } +}; + +const getShadow = shadowColor => getShadowStyle( { + shadowColor, + offsetWidth: 0, + offsetHeight: 2, + shadowOpacity: 0.25, + shadowRadius: 2, + elevation: 5 +} ); const ExploreLocationSearch = ( ): Node => { - const [query, setQuery] = useState( "" ); - const navigation = useNavigation( ); + const theme = useTheme(); + const { t } = useTranslation(); + const navigation = useNavigation(); + const queryClient = useQueryClient(); + const mapViewRef = useRef(); + const locationInput = useRef(); - const { data: placeList } = useAuthenticatedQuery( - ["fetchSearchResults", query], + const [state, dispatch] = useReducer( reducer, initialState ); + + const { + hidePlaceResults, + loading, + locationName, + mapType, + region, + place + } = state; + + const updateRegion = async newRegion => { + // don't update region if map hasn't actually moved + // otherwise, it's jittery on Android + if ( + newRegion.latitude.toFixed( 6 ) === region.latitude?.toFixed( 6 ) + && newRegion.longitude.toFixed( 6 ) === region.longitude?.toFixed( 6 ) + && newRegion.latitudeDelta.toFixed( 6 ) === region.latitudeDelta?.toFixed( 6 ) + ) { + return; + } + + dispatch( { + type: "UPDATE_REGION", + region: newRegion + } ); + }; + + const updateLocationName = useCallback( name => { + dispatch( { type: "UPDATE_LOCATION_NAME", locationName: name } ); + }, [] ); + + const setMapReady = () => dispatch( { type: "SET_LOADING", loading: false } ); + + const selectPlaceResult = newPlace => { + const { coordinates } = newPlace.point_geojson; + dispatch( { + type: "SELECT_PLACE_RESULT", + locationName: newPlace.display_name, + region: { + ...region, + latitude: coordinates[1], + longitude: coordinates[0] + // TODO: use a meaningful delta + }, + place: newPlace + } ); + }; + + // this seems necessary for clearing the cache between searches + queryClient.invalidateQueries( ["fetchSearchResults"] ); + + const { data: placeResults } = useAuthenticatedQuery( + ["fetchSearchResults", locationName], optsWithAuth => fetchSearchResults( { - q: query, + q: locationName, sources: "places", fields: "place,place.display_name,place.point_geojson" }, @@ -32,45 +159,104 @@ const ExploreLocationSearch = ( ): Node => { ) ); - const onPlaceSelected = useCallback( async newPlace => { - navigation.navigate( "Explore", { place: newPlace } ); - }, [navigation] ); - - const renderFooter = ( ) => ( - - ); - - const renderItem = useCallback( - ( { item } ) => ( - { - onPlaceSelected( item ); - }} - > - {item?.display_name} - - ), - [onPlaceSelected] + const onPlaceSelected = useCallback( + () => { + navigation.navigate( "Explore", { place } ); + }, + [navigation, place] ); return ( - - - item.id} - ListFooterComponent={renderFooter} - /> + + + + + {/* eslint-disable-next-line i18next/no-literal-string */} + + TODO: interacting with the map does not change filters + + + { + // only update location name when a user is typing, + // not when a user selects a location from the dropdown + if ( locationInput?.current?.isFocused() ) { + updateLocationName( locationText ); + } + }} + value={locationName} + testID="LocationPicker.locationSearch" + containerClass="absolute top-[20px] right-[26px] left-[26px]" + hasShadow + input={locationInput} + /> + + {!hidePlaceResults + && placeResults?.map( p => ( + { + selectPlaceResult( p ); + Keyboard.dismiss(); + }} + > + {p.display_name} + + ) )} + + + + {loading && ( + + + + )} + + { + updateRegion( newRegion ); + }} + onMapReady={setMapReady} + showCurrentLocationButton + showSwitchMapTypeButton + obsLatitude={region.latitude} + obsLongitude={region.longitude} + testID="ExploreLocationSearch.Map" + /> + + +