diff --git a/ios/Podfile b/ios/Podfile
index 725f2b6ad..d2af0c616 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -32,7 +32,7 @@ setup_permissions([
# 'Motion',
# 'Notifications',
'PhotoLibrary',
- 'PhotoLibraryAddOnly',
+ # 'PhotoLibraryAddOnly',
# 'Reminders',
# 'Siri',
# 'SpeechRecognition',
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b69bf34d1..03d48408e 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1477,12 +1477,12 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3
- DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
+ DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
FasterImage: 60d0750ddbcefff0070c4c17309c2d1d6cc650f0
FBLazyVector: 9f533d5a4c75ca77c8ed774aced1a91a0701781e
FBReactNativeSpec: 40b791f4a1df779e7e4aa12c000319f4f216d40a
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
- glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
+ glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
MMKV: 36a22a9ec84c9bb960613a089ddf6f48be9312b0
@@ -1560,7 +1560,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: bc2cdb2dc42facdf34992ae364b8a728e19a3686
RNLocalize: e8694475db034bf601e17bd3dfa8986565e769eb
- RNPermissions: b3d6efca086546e29a2920cd649a0ab04ca77794
+ RNPermissions: a123c47480a5f5d7a04d40637ad1f7360a41b465
RNReanimated: 6cfa556540186ce7ae7a0b048f369236b1d86ebb
RNScreens: b6b64d956af3715adbfe84808694ae82d3fec74f
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
@@ -1572,6 +1572,6 @@ SPEC CHECKSUMS:
VisionCameraPluginInatVision: 8480b3955bc608e913135d3bebaa57939911fb82
Yoga: c716aea2ee01df6258550c7505fa61b248145ced
-PODFILE CHECKSUM: eebd76aa39f99b44754431ed68ce0cfbfc5ec2f7
+PODFILE CHECKSUM: ebb6b37cf92e00a96e3123d4db14c5658b4e5929
COCOAPODS: 1.15.2
diff --git a/ios/iNaturalistReactNative/Info.plist b/ios/iNaturalistReactNative/Info.plist
index 3de8a7977..00b9eea28 100644
--- a/ios/iNaturalistReactNative/Info.plist
+++ b/ios/iNaturalistReactNative/Info.plist
@@ -54,7 +54,7 @@
NSAppleMusicUsageDescription
Add existing photos and sounds to your observations.
NSCameraUsageDescription
- ${PRODUCT_NAME} uses the camera to add photos to your observations of nature.
+ iNaturalist Next uses the camera to add photos to your observations of nature.
NSLocationAlwaysAndWhenInUseUsageDescription
We do not intentionally request this permission. If you are seeing this, please take a screenshot and email it to help@inaturalist.org
NSLocationWhenInUseUsageDescription
@@ -62,9 +62,9 @@
NSMicrophoneUsageDescription
Record sound observations of nature.
NSPhotoLibraryAddUsageDescription
- Export ${PRODUCT_NAME} photos to your library.
+ Export iNaturalist Next photos to your library.
NSPhotoLibraryUsageDescription
- Export and import ${PRODUCT_NAME} photos to and from your library.
+ Export and import iNaturalist Next photos to and from your library.
UIAppFonts
Lato-Bold.ttf
diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js
index e18789211..0549e927f 100644
--- a/src/components/Camera/AICamera/AICamera.js
+++ b/src/components/Camera/AICamera/AICamera.js
@@ -42,7 +42,7 @@ type Props = {
camera: Object,
device: Object,
flipCamera: Function,
- handleCheckmarkPress: Function,
+ handleCheckmarkPress: ( _result: any | null ) => void,
isLandscapeMode: boolean
};
diff --git a/src/components/Camera/AICamera/AICameraButtons.js b/src/components/Camera/AICamera/AICameraButtons.js
index c55d86825..f87e138cb 100644
--- a/src/components/Camera/AICamera/AICameraButtons.js
+++ b/src/components/Camera/AICamera/AICameraButtons.js
@@ -3,7 +3,7 @@
import CameraFlip from "components/Camera/Buttons/CameraFlip";
import Close from "components/Camera/Buttons/Close";
import Flash from "components/Camera/Buttons/Flash";
-import TakePhoto from "components/Camera/Buttons/TakePhoto";
+import TakePhoto from "components/Camera/Buttons/TakePhoto.tsx";
import Zoom from "components/Camera/Buttons/Zoom";
import TabletButtons from "components/Camera/TabletButtons";
import { View } from "components/styledComponents";
@@ -60,7 +60,7 @@ type Props = {
setNumStoredResults?: Function,
showPrediction: boolean,
showZoomButton: boolean,
- takePhoto: Function,
+ takePhoto: () => Promise,
takePhotoOptions: Object,
toggleFlash: Function,
zoomTextValue: string
diff --git a/src/components/Camera/Buttons/TakePhoto.js b/src/components/Camera/Buttons/TakePhoto.tsx
similarity index 91%
rename from src/components/Camera/Buttons/TakePhoto.js
rename to src/components/Camera/Buttons/TakePhoto.tsx
index f8d08468a..6eaf06090 100644
--- a/src/components/Camera/Buttons/TakePhoto.js
+++ b/src/components/Camera/Buttons/TakePhoto.tsx
@@ -1,5 +1,3 @@
-// @flow
-
import classnames from "classnames";
import {
INatIcon
@@ -7,7 +5,6 @@ import {
import {
Pressable, View
} from "components/styledComponents";
-import type { Node } from "react";
import React from "react";
import { useTheme } from "react-native-paper";
import { useTranslation } from "sharedHooks";
@@ -16,17 +13,17 @@ import colors from "styles/tailwindColors";
const DROP_SHADOW = getShadowForColor( colors.darkGray );
-type Props = {
- takePhoto: Function,
- disabled: boolean,
- showPrediction?: boolean
+interface Props {
+ takePhoto: () => Promise;
+ disabled: boolean;
+ showPrediction?: boolean;
}
const TakePhoto = ( {
takePhoto,
disabled,
showPrediction
-}: Props ): Node => {
+}: Props ) => {
const { t } = useTranslation( );
const theme = useTheme( );
diff --git a/src/components/Camera/CameraWithDevice.tsx b/src/components/Camera/CameraWithDevice.tsx
index cac1655a1..7b0c0b0cd 100644
--- a/src/components/Camera/CameraWithDevice.tsx
+++ b/src/components/Camera/CameraWithDevice.tsx
@@ -1,11 +1,7 @@
import { useIsFocused, useNavigation } from "@react-navigation/native";
-import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate";
import PermissionGateContainer, {
- LOCATION_PERMISSIONS,
- permissionResultFromMultiple,
- WRITE_MEDIA_PERMISSIONS
-}
- from "components/SharedComponents/PermissionGateContainer";
+ READ_WRITE_MEDIA_PERMISSIONS
+} from "components/SharedComponents/PermissionGateContainer.tsx";
import { View } from "components/styledComponents";
import React, {
useCallback, useEffect, useRef, useState
@@ -13,11 +9,6 @@ import React, {
import { StatusBar } from "react-native";
import DeviceInfo from "react-native-device-info";
import Orientation from "react-native-orientation-locker";
-import {
- checkMultiple,
- Permission,
- RESULTS
-} from "react-native-permissions";
import { Camera, CameraDevice } from "react-native-vision-camera";
// import { log } from "sharedHelpers/logger";
import { useTranslation } from "sharedHooks";
@@ -25,6 +16,7 @@ import useDeviceOrientation, {
LANDSCAPE_LEFT,
LANDSCAPE_RIGHT
} from "sharedHooks/useDeviceOrientation.ts";
+import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
import AICamera from "./AICamera/AICamera";
import usePrepareStoreAndNavigate from "./hooks/usePrepareStoreAndNavigate";
@@ -69,7 +61,16 @@ const CameraWithDevice = ( {
// permission gate on ObsEdit
const [addPhotoPermissionGateWasClosed, setAddPhotoPermissionGateWasClosed] = useState( false );
const isFocused = useIsFocused( );
- const [locationPermissionGranted, setLocationPermissionGranted] = useState( false );
+
+ // Check if location permission granted b/c usePrepareStoreAndNavigate and
+ // useUserLocation need to know if permission has been granted to fetch the
+ // user's location while the camera is active. We don't want to *ask* for
+ // permission here b/c we want to avoid overloading a new user with
+ // permission requests and they will just have seen the camera permission
+ // request before landing here, so it's ok if we're not fetching the
+ // location here for the user's first observation (suggestions might be a
+ // bit off and we'll fetch the obs coordinates on ObsEdit)
+ const { hasPermissions } = useLocationPermission( );
// logger.debug( `isFocused: ${isFocused}` );
const prepareStoreAndNavigate = usePrepareStoreAndNavigate( {
@@ -80,7 +81,7 @@ const CameraWithDevice = ( {
// up and use that to pass along to Suggestions when the user navigates
// there... but we only want to do that while the camera has focus and we
// have permission
- shouldFetchLocation: isFocused && locationPermissionGranted
+ shouldFetchLocation: isFocused && !!hasPermissions
} );
const isLandscapeMode = [LANDSCAPE_LEFT, LANDSCAPE_RIGHT].includes( deviceOrientation );
@@ -154,66 +155,25 @@ const CameraWithDevice = ( {
return unsubscribe;
}, [navigation] );
- // Check if location permission granted b/c usePrepareStoreAndNavigate and
- // useUserLocation need to know if permission has been granted to fetch the
- // user's location while the camera is active. We don't want to *ask* for
- // permission here b/c we want to avoid overloading a new user with
- // permission requests and they will just have seen the camera permission
- // request before landing here, so it's ok if we're not fetching the
- // location here for the user's first observation (suggestions might be a
- // bit off and we'll fetch the obs coordinates on ObsEdit)
- useEffect( ( ) => {
- async function checkLocationPermissions() {
- const permissionsResult = permissionResultFromMultiple(
- await checkMultiple( LOCATION_PERMISSIONS as Permission[] )
- );
- if ( permissionsResult === RESULTS.GRANTED ) {
- setLocationPermissionGranted( true );
- } else {
- console.warn(
- "Location permissions have not been granted. You probably need to use a PermissionGate"
- );
- }
- }
- checkLocationPermissions( );
- }, [] );
-
return (
- {/* TODO why is this even here? The camera doesn't need location
- permissions. Suggestions does. ~~~~kueda20240611 */}
- {/* a weird quirk of react-native-modal is you can show subsequent modals
- when a modal is nested in another modal. location permission is shown first
- because the save photo modal pops up a second system alert on iOS asking
- how much access to give */}
- setAddPhotoPermissionGateWasClosed( true )}
+ onPermissionGranted={onPhotoPermissionGranted}
+ onPermissionDenied={onPhotoPermissionDenied}
withoutNavigation
- onPermissionGranted={( ) => {
- // This probably doesn't do anything, but on the off chance we're
- // able to grab coordinates immediately after the user grants
- // permission, that will probably yield better suggestions on the
- // next screen than nothing.
- setLocationPermissionGranted( true );
- }}
- >
- setAddPhotoPermissionGateWasClosed( true )}
- onPermissionGranted={onPhotoPermissionGranted}
- onPermissionDenied={onPhotoPermissionDenied}
- withoutNavigation
- permissionNeeded={checkmarkTapped}
- />
-
+ permissionNeeded={checkmarkTapped}
+ />
{cameraType === "Standard"
? (
Promise,
}
const CameraNavButtons = ( {
diff --git a/src/components/Camera/StandardCamera/CameraOptionsButtons.js b/src/components/Camera/StandardCamera/CameraOptionsButtons.js
index 0355cabd3..7fcc23f06 100644
--- a/src/components/Camera/StandardCamera/CameraOptionsButtons.js
+++ b/src/components/Camera/StandardCamera/CameraOptionsButtons.js
@@ -13,7 +13,7 @@ import Animated from "react-native-reanimated";
const isTablet = DeviceInfo.isTablet();
type Props = {
- takePhoto: Function,
+ takePhoto: () => Promise,
handleClose: Function,
disabled: boolean,
photosTaken: boolean,
diff --git a/src/components/Camera/TabletButtons.js b/src/components/Camera/TabletButtons.js
index 960444254..95d773338 100644
--- a/src/components/Camera/TabletButtons.js
+++ b/src/components/Camera/TabletButtons.js
@@ -47,7 +47,7 @@ type Props = {
rotatableAnimatedStyle: Object,
showPrediction?: boolean,
showZoomButton: boolean,
- takePhoto: Function,
+ takePhoto: () => Promise,
takePhotoOptions: Object,
toggleFlash: Function,
zoomTextValue: string
diff --git a/src/components/Explore/Explore.js b/src/components/Explore/Explore.js
index b7f2bd919..ae4a9bd80 100644
--- a/src/components/Explore/Explore.js
+++ b/src/components/Explore/Explore.js
@@ -4,12 +4,15 @@ import { refresh } from "@react-native-community/netinfo";
import classnames from "classnames";
import ExploreFiltersModal from "components/Explore/Modals/ExploreFiltersModal";
import {
+ Body2,
+ Button,
INatIconButton,
OfflineNotice,
RadioButtonSheet,
ViewWrapper
} from "components/SharedComponents";
import { View } from "components/styledComponents";
+import { PLACE_MODE } from "providers/ExploreContext.tsx";
import type { Node } from "react";
import React, { useState } from "react";
import { Alert } from "react-native";
@@ -56,7 +59,11 @@ type Props = {
updateTaxon: Function,
updateLocation: Function,
updateUser: Function,
- updateProject: Function
+ updateProject: Function,
+ // TODO: change to PLACE_MODE in Typescript
+ placeMode: string,
+ hasLocationPermissions: ?boolean,
+ requestLocationPermissions: Function
}
const Explore = ( {
@@ -73,7 +80,10 @@ const Explore = ( {
updateTaxon,
updateLocation,
updateUser,
- updateProject
+ updateProject,
+ placeMode,
+ hasLocationPermissions,
+ requestLocationPermissions
}: Props ): Node => {
const theme = useTheme( );
const { t } = useTranslation( );
@@ -107,6 +117,69 @@ const Explore = ( {
/>
);
+ const renderMainContent = ( ) => {
+ if ( !isOnline ) {
+ return (
+ refresh()}
+ />
+ );
+ }
+ // hasLocationPermissions === undefined means we haven't checked for location permissions yet
+ if ( placeMode === PLACE_MODE.NEARBY && hasLocationPermissions === false ) {
+ return (
+
+
+ {t( "To-view-nearby-organisms-please-enable-location" )}
+
+
+ );
+ }
+ return (
+
+ {currentExploreView === "observations" && (
+
+ )}
+ {currentExploreView === "species" && (
+
+ )}
+ {currentExploreView === "observers" && (
+
+ )}
+ {currentExploreView === "identifiers" && (
+
+ )}
+
+ );
+ };
+
const renderSheet = () => {
if ( !showExploreBottomSheet ) {
return null;
@@ -170,48 +243,7 @@ const Explore = ( {
updateObservationsView={writeLayoutToStorage}
/>
)}
- { isOnline
- ? (
-
- {currentExploreView === "observations" && (
-
- )}
- {currentExploreView === "species" && (
-
- )}
- {currentExploreView === "observers" && (
-
- )}
- {currentExploreView === "identifiers" && (
-
- )}
-
- )
- : (
- refresh()}
- />
- )}
+ {renderMainContent()}
{isDebug && (
{
- const { t } = useTranslation( );
const navigation = useNavigation( );
const isOnline = useIsConnected( );
const setStoredParams = useStore( state => state.setStoredParams );
+ const {
+ hasPermissions: hasLocationPermissions,
+ renderPermissionsGate,
+ requestPermissions: requestLocationPermissions
+ } = useLocationPermission( );
+
const currentUser = useCurrentUser();
const { state, dispatch, makeSnapshot } = useExplore();
const [showFiltersModal, setShowFiltersModal] = useState( false );
- const worldwidePlaceText = t( "Worldwide" );
-
useParams( );
const updateLocation = ( place: Object ) => {
if ( place === "worldwide" ) {
+ dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_WORLDWIDE } );
dispatch( {
type: EXPLORE_ACTION.SET_PLACE,
- placeId: null,
- placeGuess: worldwidePlaceText
+ placeId: null
} );
} else {
navigation.setParams( { place } );
+ dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_PLACE } );
dispatch( {
type: EXPLORE_ACTION.SET_PLACE,
place,
@@ -93,24 +98,30 @@ const ExploreContainerWithContext = ( ): Node => {
}, [navigation, setStoredParams, state] );
return (
- dispatch( { type: EXPLORE_ACTION.FILTER_BY_ICONIC_TAXON_UNKNOWN } )
- }
- isOnline={isOnline}
- loadingStatus={loadingStatus}
- openFiltersModal={openFiltersModal}
- queryParams={queryParams}
- showFiltersModal={showFiltersModal}
- updateCount={updateCount}
- updateTaxon={taxon => dispatch( { type: EXPLORE_ACTION.CHANGE_TAXON, taxon } )}
- updateLocation={updateLocation}
- updateUser={updateUser}
- updateProject={updateProject}
- />
+ <>
+ dispatch( { type: EXPLORE_ACTION.FILTER_BY_ICONIC_TAXON_UNKNOWN } )
+ }
+ isOnline={isOnline}
+ loadingStatus={loadingStatus}
+ openFiltersModal={openFiltersModal}
+ queryParams={queryParams}
+ showFiltersModal={showFiltersModal}
+ updateCount={updateCount}
+ updateTaxon={taxon => dispatch( { type: EXPLORE_ACTION.CHANGE_TAXON, taxon } )}
+ updateLocation={updateLocation}
+ updateUser={updateUser}
+ updateProject={updateProject}
+ placeMode={state.placeMode}
+ hasLocationPermissions={hasLocationPermissions}
+ requestLocationPermissions={requestLocationPermissions}
+ />
+ {renderPermissionsGate( )}
+ >
);
};
diff --git a/src/components/Explore/Header/Header.js b/src/components/Explore/Header/Header.js
index 604d8e451..52a807406 100644
--- a/src/components/Explore/Header/Header.js
+++ b/src/components/Explore/Header/Header.js
@@ -19,6 +19,7 @@ import { Surface, useTheme } from "react-native-paper";
import { useTranslation } from "sharedHooks";
import colors from "styles/tailwindColors";
+import placeGuessText from "../helpers/placeGuessText";
import HeaderCount from "./HeaderCount";
type Props = {
@@ -49,10 +50,11 @@ const Header = ( {
const { state, numberOfFilters } = useExplore( );
const { taxon } = state;
const iconicTaxonNames = state.iconic_taxa || [];
- const placeGuess = state.place_guess;
const [showTaxonSearch, setShowTaxonSearch] = useState( false );
const [showLocationSearch, setShowLocationSearch] = useState( false );
+ const placeGuess = placeGuessText( state.placeMode, t, state.place_guess );
+
const surfaceStyle = {
backgroundColor: theme.colors.primary,
borderBottomLeftRadius: 20,
diff --git a/src/components/Explore/MapView.tsx b/src/components/Explore/MapView.tsx
index 499e08b9c..dd7bf8197 100644
--- a/src/components/Explore/MapView.tsx
+++ b/src/components/Explore/MapView.tsx
@@ -39,10 +39,6 @@ const MapView = ( {
const {
onPanDrag,
- onPermissionBlocked,
- onPermissionDenied,
- onPermissionGranted,
- permissionRequested,
onZoomToNearby,
redoSearchInMapArea,
region,
@@ -153,10 +149,6 @@ const MapView = ( {
switchMapTypeButtonClassName="left-20 bottom-20"
tileMapParams={tileMapParams}
withPressableObsTiles={tileMapParams !== null}
- onPermissionBlocked={onPermissionBlocked}
- onPermissionDenied={onPermissionDenied}
- onPermissionGranted={onPermissionGranted}
- permissionRequested={permissionRequested}
currentLocationZoomLevel={15}
/>
diff --git a/src/components/Explore/Modals/FilterModal.tsx b/src/components/Explore/Modals/FilterModal.tsx
index 814c41e64..9a2ef5ace 100644
--- a/src/components/Explore/Modals/FilterModal.tsx
+++ b/src/components/Explore/Modals/FilterModal.tsx
@@ -44,6 +44,7 @@ import { useCurrentUser, useTranslation } from "sharedHooks";
import { getShadowForColor } from "styles/global";
import colors from "styles/tailwindColors";
+import placeGuessText from "../helpers/placeGuessText";
import ExploreLocationSearchModal from "./ExploreLocationSearchModal";
import ExploreProjectSearchModal from "./ExploreProjectSearchModal";
import ExploreTaxonSearchModal from "./ExploreTaxonSearchModal";
@@ -86,8 +87,7 @@ const FilterModal = ( {
differsFromSnapshot,
discardChanges,
isNotInitialState,
- numberOfFilters,
- defaultExploreLocation
+ numberOfFilters
} = useExplore();
const {
casual,
@@ -108,6 +108,7 @@ const FilterModal = ( {
observed_on: observedOn,
photoLicense,
place_guess: placeGuess,
+ placeMode,
project,
researchGrade,
reviewedFilter,
@@ -667,8 +668,7 @@ const FilterModal = ( {
{
- const exploreLocation = await defaultExploreLocation( );
- dispatch( { type: EXPLORE_ACTION.RESET, exploreLocation } );
+ dispatch( { type: EXPLORE_ACTION.RESET } );
}}
>
{t( "Reset-verb" )}
@@ -741,31 +741,19 @@ const FilterModal = ( {
{t( "LOCATION" )}
- {placeGuess
- ? (
-
-
-
- {placeGuess}
-
-
- )
- : (
-
diff --git a/src/components/Explore/RootExploreContainer.js b/src/components/Explore/RootExploreContainer.js
index 5a06cb87d..1601e6f21 100644
--- a/src/components/Explore/RootExploreContainer.js
+++ b/src/components/Explore/RootExploreContainer.js
@@ -1,7 +1,6 @@
// @flow
import { useNavigation } from "@react-navigation/native";
-import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate";
import {
EXPLORE_ACTION,
ExploreProvider,
@@ -9,11 +8,11 @@ import {
} from "providers/ExploreContext.tsx";
import type { Node } from "react";
import React, {
- useCallback,
useEffect,
useState
} from "react";
-import { useCurrentUser, useIsConnected, useTranslation } from "sharedHooks";
+import { useCurrentUser, useIsConnected } from "sharedHooks";
+import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
import useStore from "stores/useStore";
import Explore from "./Explore";
@@ -22,29 +21,32 @@ import useHeaderCount from "./hooks/useHeaderCount";
const RootExploreContainerWithContext = ( ): Node => {
const navigation = useNavigation( );
- const { t } = useTranslation( );
const isOnline = useIsConnected( );
const currentUser = useCurrentUser( );
const rootStoredParams = useStore( state => state.rootStoredParams );
const setRootStoredParams = useStore( state => state.setRootStoredParams );
-
- const worldwidePlaceText = t( "Worldwide" );
+ const {
+ hasPermissions: hasLocationPermissions,
+ renderPermissionsGate,
+ requestPermissions: requestLocationPermissions
+ } = useLocationPermission( );
const {
- state, dispatch, makeSnapshot, defaultExploreLocation
+ state, dispatch, makeSnapshot
} = useExplore( );
const [showFiltersModal, setShowFiltersModal] = useState( false );
const updateLocation = ( place: Object ) => {
if ( place === "worldwide" ) {
+ dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_WORLDWIDE } );
dispatch( {
type: EXPLORE_ACTION.SET_PLACE,
- placeId: null,
- placeGuess: worldwidePlaceText
+ placeId: null
} );
} else {
navigation.setParams( { place } );
+ dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_PLACE } );
dispatch( {
type: EXPLORE_ACTION.SET_PLACE,
place,
@@ -90,24 +92,6 @@ const RootExploreContainerWithContext = ( ): Node => {
makeSnapshot( );
};
- const onPermissionGranted = useCallback( async ( ) => {
- const exploreLocation = await defaultExploreLocation( );
- dispatch( {
- type: EXPLORE_ACTION.SET_EXPLORE_LOCATION,
- exploreLocation
- } );
- }, [
- defaultExploreLocation,
- dispatch
- ] );
-
- const resetToWorldWide = useCallback( ( ) => {
- dispatch( {
- type: EXPLORE_ACTION.SET_PLACE,
- placeGuess: worldwidePlaceText
- } );
- }, [dispatch, worldwidePlaceText] );
-
useEffect( ( ) => {
navigation.addListener( "focus", ( ) => {
const storedState = Object.keys( rootStoredParams ).length > 0 || false;
@@ -141,14 +125,11 @@ const RootExploreContainerWithContext = ( ): Node => {
updateLocation={updateLocation}
updateUser={updateUser}
updateProject={updateProject}
+ placeMode={state.placeMode}
+ hasLocationPermissions={hasLocationPermissions}
+ requestLocationPermissions={requestLocationPermissions}
/>
-
+ {renderPermissionsGate( )}
>
);
};
diff --git a/src/components/Explore/SearchScreens/ExploreLocationSearch.js b/src/components/Explore/SearchScreens/ExploreLocationSearch.js
index 7ee9a9e81..960fa5492 100644
--- a/src/components/Explore/SearchScreens/ExploreLocationSearch.js
+++ b/src/components/Explore/SearchScreens/ExploreLocationSearch.js
@@ -9,7 +9,6 @@ import {
SearchBar,
ViewWrapper
} from "components/SharedComponents";
-import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate";
import { Pressable, View } from "components/styledComponents";
import inatPlaceTypes from "dictionaries/places";
import {
@@ -23,6 +22,7 @@ import React, {
} from "react";
import { FlatList } from "react-native";
import { useAuthenticatedQuery, useTranslation } from "sharedHooks";
+import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
import { getShadowForColor } from "styles/global";
import colors from "styles/tailwindColors";
@@ -40,7 +40,8 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
const { dispatch, defaultExploreLocation } = useExplore( );
const [locationName, setLocationName] = useState( "" );
- const [permissionNeeded, setPermissionNeeded] = useState( false );
+
+ const { hasPermissions, renderPermissionsGate, requestPermissions } = useLocationPermission( );
const resetPlace = useCallback(
( ) => {
@@ -94,6 +95,25 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
const data = placeResults || [];
+ const setNearbyLocation = useCallback( ( ) => {
+ async function getNearbyLocation( ) {
+ const exploreLocation = await defaultExploreLocation( );
+ // exploreLocation has a placeMode already
+ // dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_NEARBY } );
+ dispatch( { type: EXPLORE_ACTION.SET_EXPLORE_LOCATION, exploreLocation } );
+ closeModal();
+ }
+ getNearbyLocation( );
+ }, [dispatch, defaultExploreLocation, closeModal] );
+
+ const onNearbyPressed = () => {
+ if ( !hasPermissions ) {
+ requestPermissions( );
+ } else {
+ setNearbyLocation( );
+ }
+ };
+
return (
@@ -107,7 +127,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
/>
{t( "SEARCH-LOCATION" )}
- {t( "Reset" )}
+ {t( "Reset-verb" )}
setPermissionNeeded( true )}
+ onPress={onNearbyPressed}
text={t( "NEARBY" )}
/>
@@ -141,22 +161,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
renderItem={renderItem}
keyExtractor={item => item.id}
/>
- {
- setPermissionNeeded( false );
- const exploreLocation = await defaultExploreLocation( );
- dispatch( { type: EXPLORE_ACTION.SET_EXPLORE_LOCATION, exploreLocation } );
- closeModal();
- }}
- onPermissionDenied={( ) => {
- setPermissionNeeded( false );
- }}
- onPermissionBlocked={( ) => {
- setPermissionNeeded( false );
- }}
- />
+ {renderPermissionsGate( { onPermissionGranted: setNearbyLocation } )}
);
};
diff --git a/src/components/Explore/helpers/mapParamsToAPI.js b/src/components/Explore/helpers/mapParamsToAPI.js
index 5f5e41aa7..cbe8d9944 100644
--- a/src/components/Explore/helpers/mapParamsToAPI.js
+++ b/src/components/Explore/helpers/mapParamsToAPI.js
@@ -118,6 +118,7 @@ const mapParamsToAPI = ( params: Object, currentUser: Object ): Object => {
delete filteredParams.taxon;
delete filteredParams.place_guess;
+ delete filteredParams.placeMode;
delete filteredParams.user;
delete filteredParams.project;
delete filteredParams.sortBy;
diff --git a/src/components/Explore/helpers/placeGuessText.ts b/src/components/Explore/helpers/placeGuessText.ts
new file mode 100644
index 000000000..934b5a9c7
--- /dev/null
+++ b/src/components/Explore/helpers/placeGuessText.ts
@@ -0,0 +1,25 @@
+import { PLACE_MODE } from "providers/ExploreContext.tsx";
+
+// eslint-disable-next-line max-len
+function placeGuessText( placeMode: PLACE_MODE, t: ( _key: string ) => string, exploreStatePlaceGuess: string ): string {
+ let placeGuess = "";
+ switch ( placeMode ) {
+ case PLACE_MODE.NEARBY:
+ placeGuess = t( "Nearby" );
+ break;
+ case PLACE_MODE.WORLDWIDE:
+ placeGuess = t( "Worldwide" );
+ break;
+ case PLACE_MODE.MAP_AREA:
+ placeGuess = t( "Map-Area" );
+ break;
+ case PLACE_MODE.PLACE:
+ placeGuess = exploreStatePlaceGuess;
+ break;
+ default:
+ break;
+ }
+ return placeGuess;
+}
+
+export default placeGuessText;
diff --git a/src/components/Explore/hooks/useMapLocation.ts b/src/components/Explore/hooks/useMapLocation.ts
index 1296af8f5..5b0824897 100644
--- a/src/components/Explore/hooks/useMapLocation.ts
+++ b/src/components/Explore/hooks/useMapLocation.ts
@@ -1,7 +1,7 @@
import { useFocusEffect, useRoute } from "@react-navigation/native";
-import { RealmContext } from "providers/contexts";
import {
EXPLORE_ACTION,
+ PLACE_MODE,
useExplore
} from "providers/ExploreContext.tsx";
import {
@@ -9,41 +9,34 @@ import {
} from "react";
import { BoundingBox, Region } from "react-native-maps";
// import { log } from "sharedHelpers/logger";
-import safeRealmWrite from "sharedHelpers/safeRealmWrite";
-import { useTranslation } from "sharedHooks";
import { initialMapRegion } from "stores/createExploreSlice.ts";
import useCurrentMapRegion from "./useCurrentMapRegion";
// const logger = log.extend( "useMapLocation" );
-const { useRealm } = RealmContext;
-
const useMapLocation = ( ) => {
const { params } = useRoute( );
const worldwide = params?.worldwide;
- const realm = useRealm( );
const { dispatch, state } = useExplore( );
const [mapBoundaries, setMapBoundaries] = useState<{
swlat: number | undefined;
swlng: number | undefined;
nelat: number | undefined;
nelng: number | undefined;
- place_guess: string;
}>( );
const [showMapBoundaryButton, setShowMapBoundaryButton] = useState( false );
- const [permissionRequested, setPermissionRequested] = useState( );
const { currentMapRegion, setCurrentMapRegion } = useCurrentMapRegion( );
const place = state?.place;
const hasPlace = state.swlat || state.place_id || state.lat;
const [startAtNearby, setStartAtNearby] = useState( !hasPlace && !worldwide );
- const { t } = useTranslation( );
const onPanDrag = ( ) => setShowMapBoundaryButton( true );
- const mapWasReset = state.place_guess === t( "Nearby" ) || state.place_guess === t( "Worldwide" );
+ const mapWasReset = state.placeMode === PLACE_MODE.NEARBY
+ || state.placeMode === PLACE_MODE.WORLDWIDE;
const placeIdWasSet = state.place_id;
// eslint-disable-next-line max-len
@@ -52,15 +45,13 @@ const useMapLocation = ( ) => {
swlat: boundaries?.southWest?.latitude,
swlng: boundaries?.southWest?.longitude,
nelat: boundaries?.northEast?.latitude,
- nelng: boundaries?.northEast?.longitude,
- place_guess: t( "Map-Area" )
+ nelng: boundaries?.northEast?.longitude
};
setMapBoundaries( boundaryAPIParams );
setCurrentMapRegion( newRegion );
return boundaryAPIParams;
}, [
- t,
setMapBoundaries,
setCurrentMapRegion
] );
@@ -68,6 +59,7 @@ const useMapLocation = ( ) => {
const redoSearchInMapArea = ( ) => {
if ( !mapBoundaries ) return;
setShowMapBoundaryButton( false );
+ dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_MAP_AREA } );
dispatch( { type: EXPLORE_ACTION.SET_MAP_BOUNDARIES, mapBoundaries } );
};
@@ -77,26 +69,10 @@ const useMapLocation = ( ) => {
}, [] )
);
- useEffect( ( ) => {
- // ensure LocationPermissionGate only pops up on fresh install of the app
- const localPrefs = realm.objects( "LocalPreferences" )[0];
- if ( !localPrefs || localPrefs?.explore_location_permission_shown === false ) {
- // logger.debug( "showing LocationPermissionGate in Explore, first install only" );
- setPermissionRequested( true );
- safeRealmWrite( realm, ( ) => {
- if ( !localPrefs ) {
- realm.create( "LocalPreferences", { explore_location_permission_shown: true } );
- } else {
- localPrefs.explore_location_permission_shown = true;
- }
- }, "setting explore location permission shown to true in ExploreContainer" );
- }
- }, [realm] );
-
// eslint-disable-next-line max-len
const onZoomToNearby = useCallback( async ( newRegion: Region, nearbyBoundaries: BoundingBox | undefined ) => {
const newMapBoundaries = await updateMapBoundaries( newRegion, nearbyBoundaries );
- newMapBoundaries.place_guess = t( "Nearby" );
+ dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_NEARBY } );
dispatch( {
type: EXPLORE_ACTION.SET_MAP_BOUNDARIES,
mapBoundaries: newMapBoundaries
@@ -104,28 +80,10 @@ const useMapLocation = ( ) => {
setStartAtNearby( false );
}, [
dispatch,
- updateMapBoundaries,
- t
+ updateMapBoundaries
] );
- // PermissionGate callbacks need to use useCallback, otherwise they'll
- // trigger re-renders if/when they change
- const onPermissionGranted = useCallback( ( ) => {
- // logger.debug( "onPermissionGranted" );
- setPermissionRequested( false );
- }, [setPermissionRequested] );
-
- const onPermissionBlocked = useCallback( ( ) => {
- // logger.debug( "onPermissionBlocked" );
- setPermissionRequested( false );
- }, [setPermissionRequested] );
-
- const onPermissionDenied = useCallback( ( ) => {
- // logger.debug( "onPermissionDenied" );
- setPermissionRequested( false );
- }, [setPermissionRequested] );
-
- const previousPlaceGuess = useRef( state.place_guess );
+ const previousPlaceGuess = useRef( state.placeMode );
useEffect( ( ) => {
// region gets set when a user is navigating from ExploreLocationSearch
if ( placeIdWasSet ) {
@@ -137,9 +95,9 @@ const useMapLocation = ( ) => {
longitude: coordinates[0]
} );
} else if ( mapWasReset ) {
- // map gets set or reset back to nearby/worldwide, but only if the place_guess
+ // map gets set or reset back to nearby/worldwide, but only if the placeMode
// has changed
- if ( previousPlaceGuess.current === state.place_guess ) {
+ if ( previousPlaceGuess.current === state.placeMode ) {
return;
}
// logger.debug( "setting initial nearby or worldwide map region" );
@@ -148,7 +106,7 @@ const useMapLocation = ( ) => {
latitude: state?.lat,
longitude: state?.lng
} );
- previousPlaceGuess.current = state.place_guess;
+ previousPlaceGuess.current = state.placeMode;
}
}, [
mapWasReset,
@@ -160,11 +118,7 @@ const useMapLocation = ( ) => {
return {
onPanDrag,
- onPermissionBlocked,
- onPermissionDenied,
- onPermissionGranted,
onZoomToNearby,
- permissionRequested,
redoSearchInMapArea,
region: currentMapRegion,
showMapBoundaryButton,
diff --git a/src/components/Explore/hooks/useParams.js b/src/components/Explore/hooks/useParams.js
index 5950cd0d9..67c89ae04 100644
--- a/src/components/Explore/hooks/useParams.js
+++ b/src/components/Explore/hooks/useParams.js
@@ -6,25 +6,21 @@ import {
useExplore
} from "providers/ExploreContext.tsx";
import { useCallback, useEffect } from "react";
-import { useTranslation } from "sharedHooks";
import useStore from "stores/useStore";
const useParams = ( ): Object => {
- const { t } = useTranslation( );
const { params } = useRoute( );
const { dispatch, defaultExploreLocation } = useExplore( );
const storedParams = useStore( state => state.storedParams );
- const worldwidePlaceText = t( "Worldwide" );
-
const updateContextWithParams = useCallback( async ( storedState = { } ) => {
const setWorldwide = ( ) => {
+ dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_WORLDWIDE } );
dispatch( {
type: EXPLORE_ACTION.SET_PLACE,
storedState,
place: null,
- placeId: null,
- placeGuess: worldwidePlaceText
+ placeId: null
} );
};
@@ -33,6 +29,8 @@ const useParams = ( ): Object => {
}
if ( params?.nearby ) {
const exploreLocation = await defaultExploreLocation( );
+ // exploreLocation has a placeMode already
+ // dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_NEARBY } );
dispatch( {
type: EXPLORE_ACTION.SET_EXPLORE_LOCATION,
exploreLocation
@@ -48,6 +46,7 @@ const useParams = ( ): Object => {
} );
}
if ( params?.place ) {
+ dispatch( { type: EXPLORE_ACTION.SET_PLACE_MODE_PLACE } );
dispatch( {
type: EXPLORE_ACTION.SET_PLACE,
storedState,
@@ -75,8 +74,7 @@ const useParams = ( ): Object => {
}, [
dispatch,
params,
- defaultExploreLocation,
- worldwidePlaceText
+ defaultExploreLocation
] );
useEffect( ( ) => {
diff --git a/src/components/LocationPicker/LocationSearch.js b/src/components/LocationPicker/LocationSearch.tsx
similarity index 81%
rename from src/components/LocationPicker/LocationSearch.js
rename to src/components/LocationPicker/LocationSearch.tsx
index f1642be90..ba3afcba2 100644
--- a/src/components/LocationPicker/LocationSearch.js
+++ b/src/components/LocationPicker/LocationSearch.tsx
@@ -1,5 +1,3 @@
-// @flow
-
import { useQueryClient } from "@tanstack/react-query";
import fetchSearchResults from "api/search";
import {
@@ -7,27 +5,33 @@ import {
SearchBar
} from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
-import type { Node } from "react";
import React, { useRef } from "react";
-import { Keyboard } from "react-native";
+import { Keyboard, TextInput } from "react-native";
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
import { getShadowForColor } from "styles/global";
import colors from "styles/tailwindColors";
const DROP_SHADOW = getShadowForColor( colors.darkGray );
-type Props = {
- locationName: string,
- updateLocationName: Function,
- selectPlaceResult: Function,
- hidePlaceResults: boolean
-};
+interface Place {
+ id: string;
+ display_name: string;
+ point_geojson: {
+ coordinates: [number, number];
+ };
+}
+interface Props {
+ locationName: string;
+ updateLocationName: ( _text: string ) => void;
+ selectPlaceResult: ( _place: Place ) => void;
+ hidePlaceResults: boolean;
+}
const LocationSearch = ( {
locationName = "", updateLocationName, selectPlaceResult, hidePlaceResults
-}: Props ): Node => {
+}: Props ) => {
const queryClient = useQueryClient( );
- const locationInput = useRef( );
+ const locationInput = useRef( );
// this seems necessary for clearing the cache between searches
queryClient.invalidateQueries( { queryKey: ["fetchSearchResults"] } );
@@ -46,6 +50,7 @@ const LocationSearch = ( {
return (
<>
{
// only update location name when a user is typing,
// not when a user selects a location from the dropdown
@@ -63,7 +68,7 @@ const LocationSearch = ( {
className="absolute top-[65px] right-[26px] left-[26px] bg-white rounded-lg z-100"
style={DROP_SHADOW}
>
- {!hidePlaceResults && placeResults?.map( place => (
+ {!hidePlaceResults && placeResults?.map( ( place: Place ) => (
{
obsLongitude={longitude}
obscured={isObscured}
openMapScreen={openMapScreen}
- permissionRequested={false}
positionalAccuracy={positionalAccuracy}
scrollEnabled={false}
showLocationIndicator
diff --git a/src/components/ObsEdit/EvidenceSection.js b/src/components/ObsEdit/EvidenceSection.js
index 643638cbb..6ddc526c1 100644
--- a/src/components/ObsEdit/EvidenceSection.js
+++ b/src/components/ObsEdit/EvidenceSection.js
@@ -7,11 +7,10 @@ import {
ActivityIndicator,
Body3, Body4, Heading4, INatIcon
} from "components/SharedComponents";
-import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate";
import { MAX_SOUNDS_ALLOWED } from "components/SoundRecorder/SoundRecorder";
import { Pressable, View } from "components/styledComponents";
import type { Node } from "react";
-import React from "react";
+import React, { useCallback } from "react";
import { useTheme } from "react-native-paper";
import useTranslation from "sharedHooks/useTranslation";
@@ -22,11 +21,7 @@ import AddEvidenceSheet from "./Sheets/AddEvidenceSheet";
type Props = {
currentObservation: Object,
isFetchingLocation: boolean,
- locationPermissionNeeded: boolean,
locationTextClassNames: Array,
- onLocationPermissionBlocked: Function,
- onLocationPermissionDenied: Function,
- onLocationPermissionGranted: Function,
passesEvidenceTest: Function,
observationPhotos: Array