diff --git a/.github/workflows/e2e_android.yml b/.github/workflows/e2e_android.yml
index ddfc70cc7..fed851e2b 100644
--- a/.github/workflows/e2e_android.yml
+++ b/.github/workflows/e2e_android.yml
@@ -83,7 +83,8 @@ jobs:
E2E_TEST_USERNAME: ${{ secrets.E2E_TEST_USERNAME }}
E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
JWT_ANONYMOUS_API_SECRET: ${{ secrets.JWT_ANONYMOUS_API_SECRET }}
- run: printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\n' "JWT_ANONYMOUS_API_SECRET" "$OAUTH_CLIENT_ID" "$OAUTH_CLIENT_SECRET" "$E2E_TEST_USERNAME" "$E2E_TEST_PASSWORD" > .env
+ GMAPS_API_KEY: ${{ secrets.GMAPS_API_KEY }}
+ run: printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\nGMAPS_API_KEY=%s\n' "JWT_ANONYMOUS_API_SECRET" "$OAUTH_CLIENT_ID" "$OAUTH_CLIENT_SECRET" "$E2E_TEST_USERNAME" "$E2E_TEST_PASSWORD" "$GMAPS_API_KEY" > .env
- name: Create keystore.properties file
env:
ANDROID_KEY_STORE_PASSWORD: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
diff --git a/.gitignore b/.gitignore
index 45c103df5..20596ecb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,4 +78,4 @@ artifacts/
*.log
# VisualStudioCode #
-.vscode
+.vscode
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 0fcec44ed..a64e9eff8 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -129,6 +129,7 @@ android {
// Detox Android setup
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ manifestPlaceholders = [ GMAPS_API_KEY:project.env.get("GMAPS_API_KEY") ]
// Detox end
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c9d153cb7..43e85432c 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -17,6 +17,8 @@
android:theme="@style/AppTheme"
android:requestLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config">
+
+
{
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 43b773845..03d6c3a97 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -293,7 +293,7 @@ PODS:
- React-Core
- react-native-mail (6.1.1):
- React-Core
- - react-native-maps (0.31.1):
+ - react-native-maps (1.7.1):
- React-Core
- react-native-netinfo (9.3.9):
- React-Core
@@ -723,7 +723,7 @@ SPEC CHECKSUMS:
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
react-native-keep-awake: acbee258db16483744910f0da3ace39eb9ab47fd
react-native-mail: 8fdcd3aef007c33a6877a18eb4cf7447a1d4ce4a
- react-native-maps: 8b8bfada2c86205a7f5a07dd1f92f29b33ea83aa
+ react-native-maps: 667f9b975549c6fa9b1631bf859440f68ebd3b8f
react-native-netinfo: 22c082970cbd99071a4e5aa7a612ac20d66b08f0
react-native-orientation-locker: 851f6510d8046ea2f14aa169b1e01fcd309a94ba
react-native-render-html: 984dfe2294163d04bf5fe25d7c9f122e60e05ebe
@@ -765,4 +765,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 1e393412e8a65a884dff99189d809b73ea234bcd
-COCOAPODS: 1.11.3
+COCOAPODS: 1.12.0
diff --git a/package-lock.json b/package-lock.json
index 506c1b012..edd64402a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -71,7 +71,7 @@
"react-native-localize": "^2.2.6",
"react-native-logs": "^5.0.1",
"react-native-mail": "github:chirag04/react-native-mail",
- "react-native-maps": "^0.31.1",
+ "react-native-maps": "^1.7.1",
"react-native-modal": "^13.0.1",
"react-native-modal-datetime-picker": "^15.0.0",
"react-native-network-logger": "^1.14.1",
@@ -8893,16 +8893,6 @@
"node": ">= 0.8"
}
},
- "node_modules/deprecated-react-native-prop-types": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz",
- "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==",
- "dependencies": {
- "@react-native/normalize-color": "*",
- "invariant": "*",
- "prop-types": "*"
- }
- },
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -21525,16 +21515,15 @@
"license": "MIT"
},
"node_modules/react-native-maps": {
- "version": "0.31.1",
- "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-0.31.1.tgz",
- "integrity": "sha512-vipeOPykqLRMCLcLUCZEB+cTdNSlq88NLb0jChY4UGTY5fgOS7GYWkfswy6bW1ayTRLxJS3zpMGFDUY59/ZrXA==",
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.7.1.tgz",
+ "integrity": "sha512-CHVLzL+Q2jiUGgbt4/vosxVI1SukWyaLGjD62VLgR/wZpnH4Umi9ql1bmKDPWcfj2C1QZwMU0Yc7rXTbvZUIiw==",
"dependencies": {
- "@types/geojson": "^7946.0.7",
- "deprecated-react-native-prop-types": "^2.3.0"
+ "@types/geojson": "^7946.0.10"
},
"peerDependencies": {
- "react": ">= 16.0",
- "react-native": ">= 0.51",
+ "react": ">= 17.0.1",
+ "react-native": ">= 0.64.3",
"react-native-web": ">= 0.11"
},
"peerDependenciesMeta": {
@@ -31556,16 +31545,6 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
- "deprecated-react-native-prop-types": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz",
- "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==",
- "requires": {
- "@react-native/normalize-color": "*",
- "invariant": "*",
- "prop-types": "*"
- }
- },
"destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -40876,12 +40855,11 @@
"from": "react-native-mail@github:chirag04/react-native-mail"
},
"react-native-maps": {
- "version": "0.31.1",
- "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-0.31.1.tgz",
- "integrity": "sha512-vipeOPykqLRMCLcLUCZEB+cTdNSlq88NLb0jChY4UGTY5fgOS7GYWkfswy6bW1ayTRLxJS3zpMGFDUY59/ZrXA==",
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.7.1.tgz",
+ "integrity": "sha512-CHVLzL+Q2jiUGgbt4/vosxVI1SukWyaLGjD62VLgR/wZpnH4Umi9ql1bmKDPWcfj2C1QZwMU0Yc7rXTbvZUIiw==",
"requires": {
- "@types/geojson": "^7946.0.7",
- "deprecated-react-native-prop-types": "^2.3.0"
+ "@types/geojson": "^7946.0.10"
}
},
"react-native-modal": {
diff --git a/package.json b/package.json
index f3bf7ddee..1f449c1ce 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,7 @@
"react-native-localize": "^2.2.6",
"react-native-logs": "^5.0.1",
"react-native-mail": "github:chirag04/react-native-mail",
- "react-native-maps": "^0.31.1",
+ "react-native-maps": "^1.7.1",
"react-native-modal": "^13.0.1",
"react-native-modal-datetime-picker": "^15.0.0",
"react-native-network-logger": "^1.14.1",
diff --git a/src/components/LocationPicker/CrosshairCircle.js b/src/components/LocationPicker/CrosshairCircle.js
new file mode 100644
index 000000000..0243bdd9d
--- /dev/null
+++ b/src/components/LocationPicker/CrosshairCircle.js
@@ -0,0 +1,57 @@
+// @flow
+
+import classnames from "classnames";
+import { INatIcon } from "components/SharedComponents";
+import { View } from "components/styledComponents";
+import type { Node } from "react";
+import React from "react";
+import { useTheme } from "react-native-paper";
+
+type Props = {
+ accuracyTest: string,
+ containerStyle: Object
+};
+
+const CrosshairCircle = ( { accuracyTest, containerStyle }: Props ): Node => {
+ const theme = useTheme( );
+
+ return (
+
+
+ {/* vertical crosshair */}
+
+ {/* horizontal crosshair */}
+
+
+
+ {accuracyTest === "pass" && (
+
+ )}
+ {accuracyTest === "fail" && (
+
+ )}
+
+
+ );
+};
+
+export default CrosshairCircle;
diff --git a/src/components/LocationPicker/DisplayLatLng.js b/src/components/LocationPicker/DisplayLatLng.js
new file mode 100644
index 000000000..176aedeb8
--- /dev/null
+++ b/src/components/LocationPicker/DisplayLatLng.js
@@ -0,0 +1,47 @@
+// @flow
+
+import {
+ Body4
+} from "components/SharedComponents";
+import { View } from "components/styledComponents";
+import type { Node } from "react";
+import React from "react";
+import { useTheme } from "react-native-paper";
+
+type Props = {
+ region: Object,
+ accuracy: number,
+ getShadow: Function
+};
+
+const DisplayLatLng = ( { region, accuracy, getShadow }: Props ): Node => {
+ const theme = useTheme( );
+ const formatDecimal = coordinate => coordinate && coordinate.toFixed( 6 );
+
+ const displayLocation = ( ) => {
+ let location = "";
+ if ( region.latitude ) {
+ location += `Lat: ${formatDecimal( region.latitude )}`;
+ }
+ if ( region.longitude ) {
+ location += `, Lon: ${formatDecimal( region.longitude )}`;
+ }
+ if ( accuracy ) {
+ location += `, Acc: ${accuracy.toFixed( 0 )}`;
+ }
+ return location;
+ };
+
+ return (
+
+
+ {displayLocation( )}
+
+
+ );
+};
+
+export default DisplayLatLng;
diff --git a/src/components/LocationPicker/Footer.js b/src/components/LocationPicker/Footer.js
new file mode 100644
index 000000000..49bfaa23a
--- /dev/null
+++ b/src/components/LocationPicker/Footer.js
@@ -0,0 +1,45 @@
+// @flow
+
+import { useNavigation } from "@react-navigation/native";
+import {
+ Button, StickyToolbar
+} from "components/SharedComponents";
+import { ObsEditContext } from "providers/contexts";
+import type { Node } from "react";
+import React, {
+ useContext
+} from "react";
+import { Platform } from "react-native";
+import useTranslation from "sharedHooks/useTranslation";
+
+type Props = {
+ keysToUpdate: Object,
+ goBackOnSave: boolean
+};
+
+const Footer = ( { keysToUpdate, goBackOnSave }: Props ): Node => {
+ const navigation = useNavigation( );
+ const { t } = useTranslation( );
+ const {
+ updateObservationKeys
+ } = useContext( ObsEditContext );
+
+ return (
+
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/LocationPicker/LocationPicker.js b/src/components/LocationPicker/LocationPicker.js
new file mode 100644
index 000000000..b6bdd797c
--- /dev/null
+++ b/src/components/LocationPicker/LocationPicker.js
@@ -0,0 +1,191 @@
+// @flow
+
+import { useNavigation } from "@react-navigation/native";
+import {
+ CloseButton, Heading4,
+ ViewWrapper
+} from "components/SharedComponents";
+import { View } from "components/styledComponents";
+import { ObsEditContext } from "providers/contexts";
+import type { Node } from "react";
+import React, {
+ useCallback,
+ useContext, useEffect, useRef, useState
+} from "react";
+import { Dimensions } from "react-native";
+import MapView from "react-native-maps";
+import { IconButton, useTheme } from "react-native-paper";
+import fetchPlaceName from "sharedHelpers/fetchPlaceName";
+import fetchUserLocation from "sharedHelpers/fetchUserLocation";
+import useTranslation from "sharedHooks/useTranslation";
+import { getShadowStyle } from "styles/global";
+
+import CrosshairCircle from "./CrosshairCircle";
+import DisplayLatLng from "./DisplayLatLng";
+import Footer from "./Footer";
+import LocationSearch from "./LocationSearch";
+import WarningText from "./WarningText";
+
+const getShadow = shadowColor => getShadowStyle( {
+ shadowColor,
+ offsetWidth: 0,
+ offsetHeight: 2,
+ shadowOpacity: 0.25,
+ shadowRadius: 2,
+ elevation: 5
+} );
+
+const { width, height } = Dimensions.get( "screen" );
+
+const DELTA = 0.2;
+const CROSSHAIRLENGTH = 254;
+const DESIRED_LOCATION_ACCURACY = 4000000;
+
+type Props = {
+ route: {
+ params: {
+ goBackOnSave: boolean
+ },
+ },
+};
+
+const centerCrosshair = ( height / 2 ) - CROSSHAIRLENGTH + 30;
+
+const LocationPicker = ( { route }: Props ): Node => {
+ const theme = useTheme( );
+ const { t } = useTranslation( );
+ const mapView = useRef( );
+ const { currentObservation } = useContext( ObsEditContext );
+ const navigation = useNavigation( );
+ const { goBackOnSave } = route.params;
+ const [mapType, setMapType] = useState( "standard" );
+ const [locationName, setLocationName] = useState( currentObservation?.place_guess );
+ const [accuracy, setAccuracy] = useState( currentObservation?.positional_accuracy );
+ const [accuracyTest, setAccuracyTest] = useState( "pass" );
+ const [region, setRegion] = useState( {
+ latitude: currentObservation?.latitude || 0.0,
+ longitude: currentObservation?.longitude || 0.0,
+ latitudeDelta: DELTA,
+ longitudeDelta: DELTA
+ } );
+
+ const keysToUpdate = {
+ latitude: region.latitude,
+ longitude: region.longitude,
+ positional_accuracy: accuracy,
+ place_guess: locationName
+ };
+
+ useEffect( ( ) => {
+ if ( accuracy < 10 ) {
+ setAccuracyTest( "pass" );
+ } else if ( accuracy < DESIRED_LOCATION_ACCURACY ) {
+ setAccuracyTest( "acceptable" );
+ } else {
+ setAccuracyTest( "fail" );
+ }
+ }, [accuracy] );
+
+ const updateRegion = async newRegion => {
+ const estimatedAccuracy = newRegion.longitudeDelta * 1000 * (
+ ( CROSSHAIRLENGTH / width ) * 100
+ );
+
+ const placeName = await fetchPlaceName( newRegion.latitude, newRegion.longitude );
+ if ( placeName ) {
+ setLocationName( placeName );
+ }
+ setRegion( newRegion );
+ setAccuracy( estimatedAccuracy );
+ };
+
+ const renderBackButton = useCallback(
+ ( ) => ,
+ []
+ );
+
+ useEffect( ( ) => {
+ const renderHeaderTitle = ( ) => {t( "EDIT-LOCATION" )};
+
+ const headerOptions = {
+ headerRight: renderBackButton,
+ headerTitle: renderHeaderTitle
+ };
+
+ navigation.setOptions( headerOptions );
+ }, [renderBackButton, navigation, t] );
+
+ const toggleMapLayer = ( ) => {
+ if ( mapType === "standard" ) {
+ setMapType( "satellite" );
+ } else {
+ setMapType( "standard" );
+ }
+ };
+
+ const returnToUserLocation = async ( ) => {
+ const userLocation = await fetchUserLocation( );
+ setRegion( {
+ ...region,
+ latitude: userLocation?.latitude,
+ longitude: userLocation?.longitude
+ } );
+ };
+
+ return (
+
+ {
+ updateRegion( newRegion );
+ // console.log( await mapView?.current?.getMapBoundaries( ) );
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LocationPicker;
diff --git a/src/components/LocationPicker/LocationSearch.js b/src/components/LocationPicker/LocationSearch.js
new file mode 100644
index 000000000..4ec0e0f2e
--- /dev/null
+++ b/src/components/LocationPicker/LocationSearch.js
@@ -0,0 +1,83 @@
+// @flow
+
+import { useQueryClient } from "@tanstack/react-query";
+import fetchSearchResults from "api/search";
+import {
+ Body3,
+ SearchBar
+} from "components/SharedComponents";
+import { Pressable, View } from "components/styledComponents";
+import type { Node } from "react";
+import React, { useState } from "react";
+import { useTheme } from "react-native-paper";
+import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
+
+type Props = {
+ region: Object,
+ setRegion: Function,
+ locationName: string,
+ setLocationName: Function,
+ getShadow: Function
+};
+
+const LocationSearch = ( {
+ region, setRegion, locationName, setLocationName, getShadow
+}: Props ): Node => {
+ const [hideResults, setHideResults] = useState( false );
+ const theme = useTheme( );
+ const queryClient = useQueryClient( );
+
+ // this seems necessary for clearing the cache between searches
+ queryClient.invalidateQueries( ["fetchSearchResults"] );
+
+ const {
+ data: placeResults
+ } = useAuthenticatedQuery(
+ ["fetchSearchResults", locationName],
+ optsWithAuth => fetchSearchResults( {
+ q: locationName,
+ sources: "places",
+ fields: "place,place.display_name,place.point_geojson"
+ }, optsWithAuth )
+ );
+
+ return (
+ <>
+ {
+ setLocationName( locationText );
+ setHideResults( false );
+ }}
+ value={locationName}
+ testID="LocationPicker.locationSearch"
+ containerClass="absolute top-[20px] right-[26px] left-[26px]"
+ />
+
+ {!hideResults && placeResults?.map( place => (
+ {
+ setHideResults( true );
+ setLocationName( place.display_name );
+ const { coordinates } = place.point_geojson;
+ setRegion( {
+ ...region,
+ latitude: coordinates[1],
+ longitude: coordinates[0]
+ } );
+ }}
+ >
+ {place.display_name}
+
+ ) )}
+
+ >
+ );
+};
+
+export default LocationSearch;
diff --git a/src/components/LocationPicker/WarningText.js b/src/components/LocationPicker/WarningText.js
new file mode 100644
index 000000000..dd3a1bfa2
--- /dev/null
+++ b/src/components/LocationPicker/WarningText.js
@@ -0,0 +1,53 @@
+// @flow
+
+import classnames from "classnames";
+import {
+ Body3
+} from "components/SharedComponents";
+import { View } from "components/styledComponents";
+import type { Node } from "react";
+import React from "react";
+import { useTheme } from "react-native-paper";
+import useTranslation from "sharedHooks/useTranslation";
+
+type Props = {
+ accuracyTest: string,
+ getShadow: Function
+};
+
+const WarningText = ( { accuracyTest, getShadow }: Props ): Node => {
+ const theme = useTheme( );
+ const { t } = useTranslation( );
+
+ const displayWarningText = ( ) => {
+ if ( accuracyTest === "acceptable" ) {
+ return t( "Zoom-in" );
+ }
+ if ( accuracyTest === "fail" ) {
+ return t( "Location-accuracy-is-too-imprecise" );
+ }
+ return null;
+ };
+
+ return (
+
+
+
+ {displayWarningText( )}
+
+
+
+ );
+};
+
+export default WarningText;
diff --git a/src/components/ObsEdit/EvidenceSection.js b/src/components/ObsEdit/EvidenceSection.js
index 63f11156c..7cb4a972c 100644
--- a/src/components/ObsEdit/EvidenceSection.js
+++ b/src/components/ObsEdit/EvidenceSection.js
@@ -1,11 +1,12 @@
// @flow
+import { useNavigation } from "@react-navigation/native";
import { MAX_PHOTOS_ALLOWED } from "components/Camera/StandardCamera";
import MediaViewerModal from "components/MediaViewer/MediaViewerModal";
import {
Body3, Body4, Heading4, INatIcon
} from "components/SharedComponents";
-import { View } from "components/styledComponents";
+import { Pressable, View } from "components/styledComponents";
import {
differenceInCalendarYears,
isFuture,
@@ -46,6 +47,12 @@ const EvidenceSection = ( ): Node => {
obsPhoto => Photo.displayLocalOrRemoteSquarePhoto( obsPhoto.photo )
) : [];
const mountedRef = useRef( true );
+ const navigation = useNavigation( );
+
+ const navToLocationPicker = ( ) => {
+ navigation.navigate( "LocationPicker", { goBackOnSave: true } );
+ };
+
const [showAddEvidenceSheet, setShowAddEvidenceSheet] = useState( false );
const handleAddEvidence = ( ) => setShowAddEvidenceSheet( true );
@@ -216,13 +223,13 @@ const EvidenceSection = ( ): Node => {
/>
-
+
{displayPlaceName( )}
{/* $FlowIgnore */}
{displayLocation( )}
-
+
diff --git a/src/components/ObsEdit/LocationPicker.js b/src/components/ObsEdit/LocationPicker.js
deleted file mode 100644
index 4f0fe9832..000000000
--- a/src/components/ObsEdit/LocationPicker.js
+++ /dev/null
@@ -1,80 +0,0 @@
-// @flow
-
-import { Button } from "components/SharedComponents";
-import InputField from "components/SharedComponents/InputField";
-import Map from "components/SharedComponents/Map";
-import ViewWrapper from "components/SharedComponents/ViewWrapper";
-import type { Node } from "react";
-import React, { useEffect, useState } from "react";
-import { View } from "react-native";
-import fetchPlaceName from "sharedHelpers/fetchPlaceName";
-import useCoords from "sharedHooks/useCoords";
-import useTranslation from "sharedHooks/useTranslation";
-import { viewStyles } from "styles/obsEdit/locationPicker";
-
-type Props = {
- closeLocationPicker: Function,
- updateLocation: Function,
-};
-
-const LocationPicker = ( {
- closeLocationPicker,
- updateLocation
-}: Props ): Node => {
- const { t } = useTranslation( );
- const [searchQuery, setSearchQuery] = useState( "" );
- const [region, setRegion] = useState( {
- latitude: null,
- longitude: null
- } );
-
- const locationName = fetchPlaceName( region.latitude, region.longitude );
- const newCoords = useCoords( searchQuery );
-
- const updateLocationAndClose = () => {
- updateLocation( {
- latitude: region.latitude,
- longitude: region.longitude,
- placeGuess: locationName
- } );
- closeLocationPicker();
- };
-
- useEffect( () => {
- // update region when user types search term
- if ( !searchQuery ) {
- return;
- }
- if ( newCoords.latitude !== null && newCoords.latitude !== region.latitude ) {
- setRegion( newCoords );
- }
- }, [newCoords, region, searchQuery] );
-
- const updateCoords = newMapRegion => {
- setSearchQuery( "" );
- setRegion( newMapRegion );
- };
-
- return (
-
-
-
-
-
-
-
- );
-};
-
-export default LocationPicker;
diff --git a/src/components/SharedComponents/Buttons/CloseButton.js b/src/components/SharedComponents/Buttons/CloseButton.js
index c53f1241d..9514ae8d1 100644
--- a/src/components/SharedComponents/Buttons/CloseButton.js
+++ b/src/components/SharedComponents/Buttons/CloseButton.js
@@ -9,10 +9,13 @@ import { IconButton, useTheme } from "react-native-paper";
type Props = {
className?: string,
handleClose?: Function,
+ black?: boolean,
size?: number
}
-const CloseButton = ( { className, handleClose, size }: Props ): Node => {
+const CloseButton = ( {
+ className, handleClose, black, size
+}: Props ): Node => {
const navigation = useNavigation( );
const theme = useTheme( );
@@ -21,7 +24,7 @@ const CloseButton = ( { className, handleClose, size }: Props ): Node => {
icon="close"
size={size}
className={className}
- iconColor={theme.colors.background}
+ iconColor={black ? theme.colors.tertiary : theme.colors.background}
onPress={( ) => {
if ( handleClose ) {
handleClose( );
diff --git a/src/components/SharedComponents/SearchBar.js b/src/components/SharedComponents/SearchBar.js
new file mode 100644
index 000000000..3114810dd
--- /dev/null
+++ b/src/components/SharedComponents/SearchBar.js
@@ -0,0 +1,67 @@
+// @flow
+import { INatIcon } from "components/SharedComponents";
+import { View } from "components/styledComponents";
+import * as React from "react";
+import { Platform } from "react-native";
+import { TextInput, useTheme } from "react-native-paper";
+import { getShadowStyle } from "styles/global";
+
+const getShadow = shadowColor => getShadowStyle( {
+ shadowColor,
+ offsetWidth: 0,
+ offsetHeight: 2,
+ shadowOpacity: 0.25,
+ shadowRadius: 2,
+ elevation: 5
+} );
+
+type Props = {
+ containerClass?: string,
+ handleTextChange: Function,
+ value: string,
+ testID?: string
+}
+
+// Ensure this component is placed outside of scroll views
+
+const SearchBar = ( {
+ containerClass,
+ testID,
+ handleTextChange,
+ value
+}: Props ): React.Node => {
+ const theme = useTheme( );
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default SearchBar;
diff --git a/src/components/SharedComponents/index.js b/src/components/SharedComponents/index.js
index 0da982407..2846ef0ef 100644
--- a/src/components/SharedComponents/index.js
+++ b/src/components/SharedComponents/index.js
@@ -12,11 +12,13 @@ export { default as FloatingActionBar } from "./FloatingActionBar";
export { default as INatIcon } from "./INatIcon";
export { default as InlineUser } from "./InlineUser/InlineUser";
export { default as KebabMenu } from "./KebabMenu";
+export { default as Map } from "./Map";
export { default as ObservationLocation } from "./ObservationLocation";
export { default as PhotoCarousel } from "./PhotoCarousel";
export { default as PhotoCount } from "./PhotoCount";
export { default as QualityGradeStatus } from "./QualityGradeStatus/QualityGradeStatus";
export { default as ScrollViewWrapper } from "./ScrollViewWrapper";
+export { default as SearchBar } from "./SearchBar";
export { default as BottomSheet } from "./Sheets/BottomSheet";
export { default as BottomSheetStandardBackdrop } from "./Sheets/BottomSheetStandardBackdrop";
export { default as RadioButtonSheet } from "./Sheets/RadioButtonSheet";
diff --git a/src/components/UiLibrary.js b/src/components/UiLibrary.js
index b336e0814..feb6266d3 100644
--- a/src/components/UiLibrary.js
+++ b/src/components/UiLibrary.js
@@ -27,6 +27,7 @@ import {
ObservationLocation,
PhotoCount,
QualityGradeStatus,
+ SearchBar,
StickyToolbar,
Subheading1,
Tabs,
@@ -516,7 +517,7 @@ const UiLibrary = (): Node => {
ActivityItem
-
+
More Stuff!
Useless spacer at the end because height in NativeWind is confusing.
diff --git a/src/i18n/l10n/en.ftl b/src/i18n/l10n/en.ftl
index fcaa78ec8..51f6a6520 100644
--- a/src/i18n/l10n/en.ftl
+++ b/src/i18n/l10n/en.ftl
@@ -151,6 +151,8 @@ Display-Name = Display Name
Do-not-collect-stability-and-usage-data-using-third-party-services = Do not collect stability and usage data using third-party services
+EDIT-LOCATION = EDIT LOCATION
+
# Appears above the email text field
Email = email
@@ -263,6 +265,8 @@ Licensing = Licensing
Location = Location
+Location-accuracy-is-too-imprecise = Location accuracy is too imprecise to help identifiers. Please zoom in.
+
LOCATION-TOO-IMPRECISE = LOCATION TOO IMPRECISE
Log-in = Log in
@@ -542,6 +546,8 @@ RG = RG
Save = Save
+SAVE-LOCATION = SAVE LOCATION
+
SAVE-ALL = SAVE ALL
SAVE-CHANGES = SAVE CHANGES
@@ -761,6 +767,8 @@ You-will-lose-all-existing-observations = {$count ->
You-can-still-share-the-file =
You can still share the file with another app. If you can email it, please send it to { $email }
+Zoom-in = Zoom in so that the observation’s accuracy is as low as possible.
+
Your-location-uncertainty-is-over-4000km = Your location uncertainty is over 4000km, which is too high to be helpful to identifiers. Edit the location and zoom in until the accuracy circle turns green and is centered on where you observed the organism.
# Identification category
diff --git a/src/i18n/l10n/en.ftl.json b/src/i18n/l10n/en.ftl.json
index 5f1165719..38fa29d80 100644
--- a/src/i18n/l10n/en.ftl.json
+++ b/src/i18n/l10n/en.ftl.json
@@ -94,6 +94,7 @@
"Display": "Display",
"Display-Name": "Display Name",
"Do-not-collect-stability-and-usage-data-using-third-party-services": "Do not collect stability and usage data using third-party services",
+ "EDIT-LOCATION": "EDIT LOCATION",
"Email": {
"comment": "Appears above the email text field",
"val": "email"
@@ -174,6 +175,7 @@
},
"Licensing": "Licensing",
"Location": "Location",
+ "Location-accuracy-is-too-imprecise": "Location accuracy is too imprecise to help identifiers. Please zoom in.",
"LOCATION-TOO-IMPRECISE": "LOCATION TOO IMPRECISE",
"Log-in": "Log in",
"LOG-IN-TO-INATURALIST": "LOG IN TO INATURALIST",
@@ -359,6 +361,7 @@
"val": "RG"
},
"Save": "Save",
+ "SAVE-LOCATION": "SAVE LOCATION",
"SAVE-ALL": "SAVE ALL",
"SAVE-CHANGES": "SAVE CHANGES",
"Search-for-a-location": "Search for a location",
@@ -489,6 +492,7 @@
"You-must-be-logged-in-to-view-messages": "You must be logged in to view messages",
"You-will-lose-all-existing-observations": "{ $count ->\n [one] You will lose all existing observations. Would you like to discard 1 observation?\n *[other] You will lose all existing observations. Would you like to discard { $count } observations?\n}",
"You-can-still-share-the-file": "You can still share the file with another app. If you can email it, please send it to { $email }",
+ "Zoom-in": "Zoom in so that the observation’s accuracy is as low as possible.",
"Your-location-uncertainty-is-over-4000km": "Your location uncertainty is over 4000km, which is too high to be helpful to identifiers. Edit the location and zoom in until the accuracy circle turns green and is centered on where you observed the organism.",
"Category-leading": {
"comment": "Identification category",
diff --git a/src/i18n/strings.ftl b/src/i18n/strings.ftl
index fcaa78ec8..51f6a6520 100644
--- a/src/i18n/strings.ftl
+++ b/src/i18n/strings.ftl
@@ -151,6 +151,8 @@ Display-Name = Display Name
Do-not-collect-stability-and-usage-data-using-third-party-services = Do not collect stability and usage data using third-party services
+EDIT-LOCATION = EDIT LOCATION
+
# Appears above the email text field
Email = email
@@ -263,6 +265,8 @@ Licensing = Licensing
Location = Location
+Location-accuracy-is-too-imprecise = Location accuracy is too imprecise to help identifiers. Please zoom in.
+
LOCATION-TOO-IMPRECISE = LOCATION TOO IMPRECISE
Log-in = Log in
@@ -542,6 +546,8 @@ RG = RG
Save = Save
+SAVE-LOCATION = SAVE LOCATION
+
SAVE-ALL = SAVE ALL
SAVE-CHANGES = SAVE CHANGES
@@ -761,6 +767,8 @@ You-will-lose-all-existing-observations = {$count ->
You-can-still-share-the-file =
You can still share the file with another app. If you can email it, please send it to { $email }
+Zoom-in = Zoom in so that the observation’s accuracy is as low as possible.
+
Your-location-uncertainty-is-over-4000km = Your location uncertainty is over 4000km, which is too high to be helpful to identifiers. Edit the location and zoom in until the accuracy circle turns green and is centered on where you observed the organism.
# Identification category
diff --git a/src/navigation/BottomTabNavigator/CustomTabBar.js b/src/navigation/BottomTabNavigator/CustomTabBar.js
index 4ddcac99e..23ff8794b 100644
--- a/src/navigation/BottomTabNavigator/CustomTabBar.js
+++ b/src/navigation/BottomTabNavigator/CustomTabBar.js
@@ -83,6 +83,7 @@ const CustomTabBar = ( { state, descriptors, navigation }: Props ): Node => {
|| currentRoute.includes( "ObsEdit" )
|| currentRoute.includes( "AddID" )
|| currentRoute.includes( "Login" )
+ || currentRoute.includes( "LocationPicker" )
) {
return null;
}
diff --git a/src/navigation/BottomTabNavigator/index.js b/src/navigation/BottomTabNavigator/index.js
index d2ec0211a..78c89c3e2 100644
--- a/src/navigation/BottomTabNavigator/index.js
+++ b/src/navigation/BottomTabNavigator/index.js
@@ -3,6 +3,7 @@ import About from "components/About";
import StandardCamera from "components/Camera/StandardCamera";
import Explore from "components/Explore/Explore";
import Identify from "components/Identify/Identify";
+import LocationPicker from "components/LocationPicker/LocationPicker";
import Login from "components/LoginSignUp/Login";
import Messages from "components/Messages/Messages";
import MyObservationsContainer from "components/MyObservations/MyObservationsContainer";
@@ -280,6 +281,15 @@ const BottomTabs = () => {
title: t( "Add-an-ID" )
}}
/>
+
diff --git a/src/sharedHelpers/fetchCoordinates.js b/src/sharedHelpers/fetchCoordinates.js
new file mode 100644
index 000000000..ac1bad8e6
--- /dev/null
+++ b/src/sharedHelpers/fetchCoordinates.js
@@ -0,0 +1,19 @@
+// @flow
+import Geocoder from "react-native-geocoder-reborn";
+
+const fetchCoordinates = async ( locationName: ?string ): any => {
+ if ( !locationName ) { return null; }
+ try {
+ const results = await Geocoder.geocodeAddress( locationName );
+ if ( results.length === 0 ) { return null; }
+ const { position } = results[0];
+ return {
+ latitude: position.lat,
+ longitude: position.lng
+ };
+ } catch ( e ) {
+ return null;
+ }
+};
+
+export default fetchCoordinates;
diff --git a/src/sharedHelpers/fetchPlaceName.js b/src/sharedHelpers/fetchPlaceName.js
index eb9363091..5c0e0f697 100644
--- a/src/sharedHelpers/fetchPlaceName.js
+++ b/src/sharedHelpers/fetchPlaceName.js
@@ -36,9 +36,13 @@ const setPlaceName = ( results: Array