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 && (
+
+
+
+ )}
+
+
+
+
+
);
};