mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-06 22:56:12 -04:00
Changes to the way permissions are asked for (#1793)
* Replace name in permission requests * TakePhoto TS * PermissionGate TS * Type * PermissionGateContainer TS * Interface * Types * LocationGate TS * Remove LocationPermissionGate from Camera * Remove write only permission * Type * ObsPhotoSelectionList TS * Code style * Show the improve with location button * Create useLocationPermission.tsx * Use new hook on suggestions * Doc comment * Use new hook in camera view * Add strings * Refactor Explore main content * Use permission hook on RootExplore * Add no location permission component * Rename function * Prop request permissions and use with button * Default to Nearby label * Remove Node type * Projects TS * Use useLocationPermission hook in projects screen * Add string * Prop permission down * Refactor list render * Refactor tab id into enum * Tab type * On nearby tab if without permission show button to prompt * Leftovers * Remove location permission gate from ObsEdit * Use location permission hook on evidence section * SearchBar TS * Do not autoFocus on search bar in location picker. Closes #1743 * Update type * LocationSearch TS * Show location permission gate on location picker's mount * Add location permission to CurrentLocationButton * Remove unused props of Map * Remove unused exports from useMapLocation * Migration * Revert "Show location permission gate on location picker's mount" This reverts commit 30ff75698c53d54d0b14cd2bd629f7155b743bf8. * Add callbacks to useLocationPermission hook * Show location permission ask on Obs Edit * Remove unused string * Reset explore filters should set location always to worldwide * Add helper function to show place text in Explore * Remove unused state of filter modal * Show place text in filters modal with helper * Show location permission button only for Nearby explore state * Add a placeMode state * Do not send placeMode to API * Also treat limited permission as yes * useLocationPermission in ExploreLocationSearch * Refactor to setting place mode Instead of logic based on the translated text of the place_guess string that is stored in ExploreContext, we are switching to an enum state that signifies which mode to show on explore: 1.) Nearby: Filters explore results based on the user's location. This also has a state without location permission that does not query the API. 2.) Place: Filtering by a specific place (as retrieved by /places API). 3.) Worldwide: Retrieve worldwide results, i.e. not having a place filter set. 4.) Map area: Filtering explore results precisely to the map rectangle shown on the explore map. * Remove import from test * Remove export * Use blocked title only for blocked permission asks * Move gallery permission container to Tab navigator as are the others * Add gallery save title * Split location permission explanation into two * Update strings.ftl * Only nav to location picker if permission was not granted * Check permission on app being foregrounded * The location permission part is handled by useLocationPermission * Do not store permission result in hook * Use hasPermission from permissions hook * Update fetchUserLocation.e2e-mock * Move hook one higher * Show user location if permission is given * PermissionGate callbacks should use useCallback * Add permission hook to map usage * Fix test * Update layout to be asserted * Add location permission hook to Explore * Remove console.log * Few TS fixes * Indentation * Remove superficial check * Update Podfile.lock
This commit is contained in:
@@ -32,7 +32,7 @@ setup_permissions([
|
||||
# 'Motion',
|
||||
# 'Notifications',
|
||||
'PhotoLibrary',
|
||||
'PhotoLibraryAddOnly',
|
||||
# 'PhotoLibraryAddOnly',
|
||||
# 'Reminders',
|
||||
# 'Siri',
|
||||
# 'SpeechRecognition',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>Add existing photos and sounds to your observations.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>${PRODUCT_NAME} uses the camera to add photos to your observations of nature.</string>
|
||||
<string>iNaturalist Next uses the camera to add photos to your observations of nature.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>We do not intentionally request this permission. If you are seeing this, please take a screenshot and email it to help@inaturalist.org</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
@@ -62,9 +62,9 @@
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Record sound observations of nature.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Export ${PRODUCT_NAME} photos to your library.</string>
|
||||
<string>Export iNaturalist Next photos to your library.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Export and import ${PRODUCT_NAME} photos to and from your library.</string>
|
||||
<string>Export and import iNaturalist Next photos to and from your library.</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Lato-Bold.ttf</string>
|
||||
|
||||
@@ -42,7 +42,7 @@ type Props = {
|
||||
camera: Object,
|
||||
device: Object,
|
||||
flipCamera: Function,
|
||||
handleCheckmarkPress: Function,
|
||||
handleCheckmarkPress: ( _result: any | null ) => void,
|
||||
isLandscapeMode: boolean
|
||||
};
|
||||
|
||||
|
||||
@@ -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<void>,
|
||||
takePhotoOptions: Object,
|
||||
toggleFlash: Function,
|
||||
zoomTextValue: string
|
||||
|
||||
@@ -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<void>;
|
||||
disabled: boolean;
|
||||
showPrediction?: boolean;
|
||||
}
|
||||
|
||||
const TakePhoto = ( {
|
||||
takePhoto,
|
||||
disabled,
|
||||
showPrediction
|
||||
}: Props ): Node => {
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const theme = useTheme( );
|
||||
|
||||
@@ -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 (
|
||||
<View
|
||||
className={`flex-1 bg-black ${flexDirection}`}
|
||||
testID="CameraWithDevice"
|
||||
>
|
||||
{/* 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 */}
|
||||
<LocationPermissionGate
|
||||
permissionNeeded={checkmarkTapped}
|
||||
<PermissionGateContainer
|
||||
permissions={READ_WRITE_MEDIA_PERMISSIONS}
|
||||
title={t( "Save-photos-to-your-gallery" )}
|
||||
titleDenied={t( "Save-photos-to-your-gallery" )}
|
||||
body={t( "iNaturalist-can-save-photos-you-take-in-the-app-to-your-devices-gallery" )}
|
||||
buttonText={t( "SAVE-PHOTOS" )}
|
||||
icon="gallery"
|
||||
image={require( "images/birger-strahl-ksiGE4hMiso-unsplash.jpg" )}
|
||||
onModalHide={( ) => 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 );
|
||||
}}
|
||||
>
|
||||
<PermissionGateContainer
|
||||
permissions={WRITE_MEDIA_PERMISSIONS}
|
||||
titleDenied={t( "Save-photos-to-your-gallery" )}
|
||||
body={t( "iNaturalist-can-save-photos-you-take-in-the-app-to-your-devices-gallery" )}
|
||||
buttonText={t( "SAVE-PHOTOS" )}
|
||||
icon="gallery"
|
||||
image={require( "images/birger-strahl-ksiGE4hMiso-unsplash.jpg" )}
|
||||
onModalHide={( ) => setAddPhotoPermissionGateWasClosed( true )}
|
||||
onPermissionGranted={onPhotoPermissionGranted}
|
||||
onPermissionDenied={onPhotoPermissionDenied}
|
||||
withoutNavigation
|
||||
permissionNeeded={checkmarkTapped}
|
||||
/>
|
||||
</LocationPermissionGate>
|
||||
permissionNeeded={checkmarkTapped}
|
||||
/>
|
||||
{cameraType === "Standard"
|
||||
? (
|
||||
<StandardCamera
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import TakePhoto from "components/Camera/Buttons/TakePhoto";
|
||||
import TakePhoto from "components/Camera/Buttons/TakePhoto.tsx";
|
||||
import { MediaNavButtons } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
handleClose: Function,
|
||||
photosTaken: boolean,
|
||||
rotatableAnimatedStyle: Object,
|
||||
takePhoto: Function
|
||||
takePhoto: () => Promise<void>,
|
||||
}
|
||||
|
||||
const CameraNavButtons = ( {
|
||||
|
||||
@@ -13,7 +13,7 @@ import Animated from "react-native-reanimated";
|
||||
const isTablet = DeviceInfo.isTablet();
|
||||
|
||||
type Props = {
|
||||
takePhoto: Function,
|
||||
takePhoto: () => Promise<void>,
|
||||
handleClose: Function,
|
||||
disabled: boolean,
|
||||
photosTaken: boolean,
|
||||
|
||||
@@ -47,7 +47,7 @@ type Props = {
|
||||
rotatableAnimatedStyle: Object,
|
||||
showPrediction?: boolean,
|
||||
showZoomButton: boolean,
|
||||
takePhoto: Function,
|
||||
takePhoto: () => Promise<void>,
|
||||
takePhotoOptions: Object,
|
||||
toggleFlash: Function,
|
||||
zoomTextValue: string
|
||||
|
||||
@@ -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 (
|
||||
<OfflineNotice
|
||||
onPress={() => refresh()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// hasLocationPermissions === undefined means we haven't checked for location permissions yet
|
||||
if ( placeMode === PLACE_MODE.NEARBY && hasLocationPermissions === false ) {
|
||||
return (
|
||||
<View className="flex-1 justify-center p-4">
|
||||
<View className="items-center">
|
||||
<Body2>{t( "To-view-nearby-organisms-please-enable-location" )}</Body2>
|
||||
</View>
|
||||
<Button
|
||||
className="mt-5"
|
||||
text={t( "ALLOW-LOCATION-ACCESS" )}
|
||||
accessibilityHint={t( "Opens-location-permission-prompt" )}
|
||||
level="focus"
|
||||
onPress={( ) => requestLocationPermissions()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View className="flex-1">
|
||||
{currentExploreView === "observations" && (
|
||||
<ObservationsView
|
||||
count={count}
|
||||
layout={layout}
|
||||
queryParams={queryParams}
|
||||
updateCount={updateCount}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === "species" && (
|
||||
<SpeciesView
|
||||
count={count}
|
||||
isOnline={isOnline}
|
||||
queryParams={queryParams}
|
||||
updateCount={updateCount}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === "observers" && (
|
||||
<ObserversView
|
||||
count={count}
|
||||
isOnline={isOnline}
|
||||
queryParams={queryParams}
|
||||
updateCount={updateCount}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === "identifiers" && (
|
||||
<IdentifiersView
|
||||
count={count}
|
||||
isOnline={isOnline}
|
||||
queryParams={queryParams}
|
||||
updateCount={updateCount}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSheet = () => {
|
||||
if ( !showExploreBottomSheet ) {
|
||||
return null;
|
||||
@@ -170,48 +243,7 @@ const Explore = ( {
|
||||
updateObservationsView={writeLayoutToStorage}
|
||||
/>
|
||||
)}
|
||||
{ isOnline
|
||||
? (
|
||||
<View className="flex-1">
|
||||
{currentExploreView === "observations" && (
|
||||
<ObservationsView
|
||||
count={count}
|
||||
layout={layout}
|
||||
queryParams={queryParams}
|
||||
updateCount={updateCount}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === "species" && (
|
||||
<SpeciesView
|
||||
count={count}
|
||||
isOnline={isOnline}
|
||||
queryParams={queryParams}
|
||||
updateCount={updateCount}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === "observers" && (
|
||||
<ObserversView
|
||||
count={count}
|
||||
isOnline={isOnline}
|
||||
queryParams={queryParams}
|
||||
updateCount={updateCount}
|
||||
/>
|
||||
)}
|
||||
{currentExploreView === "identifiers" && (
|
||||
<IdentifiersView
|
||||
count={count}
|
||||
isOnline={isOnline}
|
||||
queryParams={queryParams}
|
||||
updateCount={updateCount}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<OfflineNotice
|
||||
onPress={() => refresh()}
|
||||
/>
|
||||
)}
|
||||
{renderMainContent()}
|
||||
{isDebug && (
|
||||
<INatIconButton
|
||||
icon="triangle-exclamation"
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
} from "providers/ExploreContext.tsx";
|
||||
import type { Node } from "react";
|
||||
import React, { 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";
|
||||
@@ -17,30 +18,34 @@ import useHeaderCount from "./hooks/useHeaderCount";
|
||||
import useParams from "./hooks/useParams";
|
||||
|
||||
const ExploreContainerWithContext = ( ): Node => {
|
||||
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 (
|
||||
<Explore
|
||||
closeFiltersModal={closeFiltersModal}
|
||||
count={count}
|
||||
hideBackButton={false}
|
||||
filterByIconicTaxonUnknown={
|
||||
() => 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}
|
||||
/>
|
||||
<>
|
||||
<Explore
|
||||
closeFiltersModal={closeFiltersModal}
|
||||
count={count}
|
||||
hideBackButton={false}
|
||||
filterByIconicTaxonUnknown={
|
||||
() => 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( )}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -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 = ( {
|
||||
<Body3
|
||||
accessibilityRole="button"
|
||||
onPress={async ( ) => {
|
||||
const exploreLocation = await defaultExploreLocation( );
|
||||
dispatch( { type: EXPLORE_ACTION.RESET, exploreLocation } );
|
||||
dispatch( { type: EXPLORE_ACTION.RESET } );
|
||||
}}
|
||||
>
|
||||
{t( "Reset-verb" )}
|
||||
@@ -741,31 +741,19 @@ const FilterModal = ( {
|
||||
<View className="mb-7">
|
||||
<Heading4 className="mb-5">{t( "LOCATION" )}</Heading4>
|
||||
<View className="mb-5">
|
||||
{placeGuess
|
||||
? (
|
||||
<View>
|
||||
<View className="flex-row items-center mb-5">
|
||||
<INatIcon name="location" size={15} />
|
||||
<Body3 className="ml-4">{placeGuess}</Body3>
|
||||
</View>
|
||||
<Button
|
||||
text={t( "EDIT-LOCATION" )}
|
||||
onPress={() => {
|
||||
setShowLocationSearchModal( true );
|
||||
}}
|
||||
accessibilityLabel={t( "Edit" )}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
text={t( "SEARCH-FOR-A-LOCATION" )}
|
||||
onPress={() => {
|
||||
setShowLocationSearchModal( true );
|
||||
}}
|
||||
accessibilityLabel={t( "Search" )}
|
||||
/>
|
||||
)}
|
||||
<View>
|
||||
<View className="flex-row items-center mb-5">
|
||||
<INatIcon name="location" size={15} />
|
||||
<Body3 className="ml-4">{placeGuessText( placeMode, t, placeGuess )}</Body3>
|
||||
</View>
|
||||
<Button
|
||||
text={t( "EDIT-LOCATION" )}
|
||||
onPress={() => {
|
||||
setShowLocationSearchModal( true );
|
||||
}}
|
||||
accessibilityLabel={t( "Edit" )}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<LocationPermissionGate
|
||||
permissionNeeded
|
||||
onPermissionGranted={onPermissionGranted}
|
||||
onPermissionDenied={resetToWorldWide}
|
||||
onPermissionBlocked={resetToWorldWide}
|
||||
withoutNavigation
|
||||
/>
|
||||
{renderPermissionsGate( )}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<ViewWrapper testID="explore-location-search">
|
||||
<View className="flex-row justify-center p-5 bg-white">
|
||||
@@ -107,7 +127,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
|
||||
/>
|
||||
<Heading4>{t( "SEARCH-LOCATION" )}</Heading4>
|
||||
<Body3 onPress={resetPlace} className="absolute top-4 right-4">
|
||||
{t( "Reset" )}
|
||||
{t( "Reset-verb" )}
|
||||
</Body3>
|
||||
</View>
|
||||
<View
|
||||
@@ -124,7 +144,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
|
||||
<View className="flex-row px-3 mt-5 justify-evenly">
|
||||
<Button
|
||||
className="w-1/2"
|
||||
onPress={( ) => setPermissionNeeded( true )}
|
||||
onPress={onNearbyPressed}
|
||||
text={t( "NEARBY" )}
|
||||
/>
|
||||
<View className="px-2" />
|
||||
@@ -141,22 +161,7 @@ const ExploreLocationSearch = ( { closeModal, updateLocation }: Props ): Node =>
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
/>
|
||||
<LocationPermissionGate
|
||||
permissionNeeded={permissionNeeded}
|
||||
withoutNavigation
|
||||
onPermissionGranted={async ( ) => {
|
||||
setPermissionNeeded( false );
|
||||
const exploreLocation = await defaultExploreLocation( );
|
||||
dispatch( { type: EXPLORE_ACTION.SET_EXPLORE_LOCATION, exploreLocation } );
|
||||
closeModal();
|
||||
}}
|
||||
onPermissionDenied={( ) => {
|
||||
setPermissionNeeded( false );
|
||||
}}
|
||||
onPermissionBlocked={( ) => {
|
||||
setPermissionNeeded( false );
|
||||
}}
|
||||
/>
|
||||
{renderPermissionsGate( { onPermissionGranted: setNearbyLocation } )}
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
25
src/components/Explore/helpers/placeGuessText.ts
Normal file
25
src/components/Explore/helpers/placeGuessText.ts
Normal file
@@ -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;
|
||||
@@ -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<boolean>( );
|
||||
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,
|
||||
|
||||
@@ -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( ( ) => {
|
||||
|
||||
@@ -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<TextInput>( );
|
||||
|
||||
// this seems necessary for clearing the cache between searches
|
||||
queryClient.invalidateQueries( { queryKey: ["fetchSearchResults"] } );
|
||||
@@ -46,6 +50,7 @@ const LocationSearch = ( {
|
||||
return (
|
||||
<>
|
||||
<SearchBar
|
||||
autoFocus={false}
|
||||
handleTextChange={locationText => {
|
||||
// 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 ) => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
key={place.id}
|
||||
@@ -65,7 +65,6 @@ const LocationSection = ( { observation }: Props ): Node => {
|
||||
obsLongitude={longitude}
|
||||
obscured={isObscured}
|
||||
openMapScreen={openMapScreen}
|
||||
permissionRequested={false}
|
||||
positionalAccuracy={positionalAccuracy}
|
||||
scrollEnabled={false}
|
||||
showLocationIndicator
|
||||
|
||||
@@ -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<string>,
|
||||
onLocationPermissionBlocked: Function,
|
||||
onLocationPermissionDenied: Function,
|
||||
onLocationPermissionGranted: Function,
|
||||
passesEvidenceTest: Function,
|
||||
observationPhotos: Array<Object>,
|
||||
setShowAddEvidenceSheet: Function,
|
||||
@@ -39,21 +34,23 @@ type Props = {
|
||||
uuid: string
|
||||
}>,
|
||||
updateObservationKeys: Function,
|
||||
renderPermissionsGate: Function,
|
||||
requestPermissions: Function,
|
||||
hasPermissions: boolean
|
||||
}
|
||||
|
||||
const EvidenceSection = ( {
|
||||
currentObservation,
|
||||
isFetchingLocation,
|
||||
locationPermissionNeeded,
|
||||
locationTextClassNames,
|
||||
onLocationPermissionBlocked,
|
||||
onLocationPermissionDenied,
|
||||
onLocationPermissionGranted,
|
||||
passesEvidenceTest,
|
||||
setShowAddEvidenceSheet,
|
||||
showAddEvidenceSheet,
|
||||
observationSounds,
|
||||
updateObservationKeys
|
||||
updateObservationKeys,
|
||||
renderPermissionsGate,
|
||||
requestPermissions,
|
||||
hasPermissions
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const theme = useTheme( );
|
||||
@@ -65,8 +62,18 @@ const EvidenceSection = ( {
|
||||
const obsSounds = currentObservation?.observationSounds || currentObservation?.observation_sounds;
|
||||
const navigation = useNavigation( );
|
||||
|
||||
const navToLocationPicker = ( ) => {
|
||||
const navToLocationPicker = useCallback( ( ) => {
|
||||
navigation.navigate( "LocationPicker", { goBackOnSave: true } );
|
||||
}, [navigation] );
|
||||
|
||||
const onLocationPress = ( ) => {
|
||||
// If we have location permissions, navigate to the location picker
|
||||
if ( hasPermissions ) {
|
||||
navToLocationPicker();
|
||||
} else {
|
||||
// If we don't have location permissions, request them
|
||||
requestPermissions( );
|
||||
}
|
||||
};
|
||||
|
||||
const latitude = currentObservation?.latitude;
|
||||
@@ -127,7 +134,7 @@ const EvidenceSection = ( {
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
className="flex-row flex-nowrap pb-3"
|
||||
onPress={navToLocationPicker}
|
||||
onPress={onLocationPress}
|
||||
accessibilityLabel={t( "Edit-location" )}
|
||||
>
|
||||
<View className="w-[30px] items-center mr-1">
|
||||
@@ -169,17 +176,19 @@ const EvidenceSection = ( {
|
||||
}
|
||||
</View>
|
||||
</Pressable>
|
||||
{renderPermissionsGate( {
|
||||
// If the user does not give location permissions in any form,
|
||||
// navigate to the location picker (if granted we just continue fetching the location)
|
||||
onRequestDenied: navToLocationPicker,
|
||||
onRequestBlocked: navToLocationPicker,
|
||||
onModalHide: ( ) => {
|
||||
if ( !hasPermissions ) navToLocationPicker();
|
||||
}
|
||||
} )}
|
||||
<DatePicker
|
||||
currentObservation={currentObservation}
|
||||
updateObservationKeys={updateObservationKeys}
|
||||
/>
|
||||
<LocationPermissionGate
|
||||
permissionNeeded={locationPermissionNeeded}
|
||||
onPermissionGranted={onLocationPermissionGranted}
|
||||
onPermissionDenied={onLocationPermissionDenied}
|
||||
onPermissionBlocked={onLocationPermissionBlocked}
|
||||
withoutNavigation
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,13 +12,12 @@ import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef, useState
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
RESULTS as PERMISSION_RESULTS
|
||||
} from "react-native-permissions";
|
||||
import fetchPlaceName from "sharedHelpers/fetchPlaceName";
|
||||
import useCurrentObservationLocation from "sharedHooks/useCurrentObservationLocation";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
import EvidenceSection from "./EvidenceSection";
|
||||
@@ -51,7 +50,8 @@ const EvidenceSectionContainer = ( {
|
||||
const [
|
||||
shouldRetryCurrentObservationLocation,
|
||||
setShouldRetryCurrentObservationLocation
|
||||
] = useState( false );
|
||||
] = useState( true );
|
||||
const { hasPermissions, renderPermissionsGate, requestPermissions } = useLocationPermission( );
|
||||
|
||||
// Hook version of componentWillUnmount. We use a ref to track mounted
|
||||
// state (not useState, which might get frozen in a closure for other
|
||||
@@ -71,14 +71,14 @@ const EvidenceSectionContainer = ( {
|
||||
|
||||
const {
|
||||
hasLocation,
|
||||
isFetchingLocation,
|
||||
permissionResult: locationPermissionResult
|
||||
isFetchingLocation
|
||||
} = useCurrentObservationLocation(
|
||||
mountedRef,
|
||||
currentObservation,
|
||||
updateObservationKeys,
|
||||
hasPermissions,
|
||||
{
|
||||
retry: shouldRetryCurrentObservationLocation
|
||||
retry: hasPermissions && shouldRetryCurrentObservationLocation
|
||||
}
|
||||
);
|
||||
|
||||
@@ -88,10 +88,8 @@ const EvidenceSectionContainer = ( {
|
||||
useEffect( ( ) => {
|
||||
if ( latitude ) {
|
||||
setShouldRetryCurrentObservationLocation( false );
|
||||
} else if ( locationPermissionResult === "granted" ) {
|
||||
setShouldRetryCurrentObservationLocation( true );
|
||||
}
|
||||
}, [latitude, locationPermissionResult] );
|
||||
}, [latitude] );
|
||||
|
||||
const hasPhotoOrSound = useMemo( ( ) => {
|
||||
if ( currentObservation?.observationPhotos?.length > 0
|
||||
@@ -220,16 +218,9 @@ const EvidenceSectionContainer = ( {
|
||||
setShowAddEvidenceSheet={setShowAddEvidenceSheet}
|
||||
showAddEvidenceSheet={showAddEvidenceSheet}
|
||||
observationSounds={observationSounds}
|
||||
onLocationPermissionGranted={( ) => {
|
||||
setShouldRetryCurrentObservationLocation( true );
|
||||
}}
|
||||
onLocationPermissionDenied={( ) => {
|
||||
setShouldRetryCurrentObservationLocation( false );
|
||||
}}
|
||||
onLocationPermissionBlocked={( ) => {
|
||||
setShouldRetryCurrentObservationLocation( false );
|
||||
}}
|
||||
locationPermissionNeeded={locationPermissionResult === PERMISSION_RESULTS.DENIED}
|
||||
hasPermissions={hasPermissions}
|
||||
renderPermissionsGate={renderPermissionsGate}
|
||||
requestPermissions={requestPermissions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@ import {
|
||||
} from "appConstants/paths.ts";
|
||||
import navigateToObsDetails from "components/ObsDetails/helpers/navigateToObsDetails";
|
||||
import { ActivityAnimation, ViewWrapper } from "components/SharedComponents";
|
||||
import PermissionGateContainer, { READ_MEDIA_PERMISSIONS }
|
||||
from "components/SharedComponents/PermissionGateContainer";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
@@ -29,7 +27,6 @@ const MAX_PHOTOS_ALLOWED = 20;
|
||||
const PhotoGallery = ( ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const [photoGalleryShown, setPhotoGalleryShown] = useState( false );
|
||||
const [permissionGranted, setPermissionGranted] = useState( false );
|
||||
const setPhotoImporterState = useStore( state => state.setPhotoImporterState );
|
||||
const setGroupedPhotos = useStore( state => state.setGroupedPhotos );
|
||||
const groupedPhotos = useStore( state => state.groupedPhotos );
|
||||
@@ -195,10 +192,6 @@ const PhotoGallery = ( ): Node => {
|
||||
params
|
||||
] );
|
||||
|
||||
const onPermissionGranted = () => {
|
||||
setPermissionGranted( true );
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback( () => {
|
||||
// This will run when the screen comes into focus.
|
||||
@@ -206,7 +199,7 @@ const PhotoGallery = ( ): Node => {
|
||||
|
||||
// Wait for screen to finish transition
|
||||
interactionHandle = InteractionManager.runAfterInteractions( () => {
|
||||
if ( permissionGranted && !photoGalleryShown ) {
|
||||
if ( !photoGalleryShown ) {
|
||||
showPhotoGallery();
|
||||
}
|
||||
} );
|
||||
@@ -217,26 +210,13 @@ const PhotoGallery = ( ): Node => {
|
||||
interactionHandle.cancel();
|
||||
}
|
||||
};
|
||||
}, [permissionGranted, photoGalleryShown, showPhotoGallery] )
|
||||
}, [photoGalleryShown, showPhotoGallery] )
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper testID="PhotoGallery">
|
||||
<View className="flex-1 w-full h-full justify-center items-center">
|
||||
<ActivityAnimation />
|
||||
{!permissionGranted && (
|
||||
<PermissionGateContainer
|
||||
permissions={READ_MEDIA_PERMISSIONS}
|
||||
title={t( "Observe-and-identify-organisms-from-your-gallery" )}
|
||||
titleDenied={t( "Please-Allow-Gallery-Access" )}
|
||||
body={t( "Upload-photos-from-your-gallery-and-create-observations" )}
|
||||
blockedPrompt={t( "Youve-previously-denied-gallery-permissions" )}
|
||||
buttonText={t( "CHOOSE-PHOTOS" )}
|
||||
icon="gallery"
|
||||
image={require( "images/viviana-rishe-j2330n6bg3I-unsplash.jpg" )}
|
||||
onPermissionGranted={onPermissionGranted}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ViewWrapper>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Body1,
|
||||
Body2,
|
||||
Button,
|
||||
Heading1,
|
||||
INatIcon,
|
||||
@@ -12,8 +11,8 @@ import {
|
||||
Tabs,
|
||||
ViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
import { Tab } from "components/SharedComponents/Tabs/Tabs.tsx";
|
||||
import { Pressable, View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
FlatList
|
||||
@@ -22,19 +21,27 @@ import {
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
|
||||
type Props = {
|
||||
searchInput: string,
|
||||
setSearchInput: Function,
|
||||
tabs: Array<Object>,
|
||||
currentTabId: string,
|
||||
projects: Array<Object>,
|
||||
isLoading: boolean,
|
||||
memberId: ?number
|
||||
import { TAB_ID } from "./ProjectsContainer";
|
||||
|
||||
interface Props {
|
||||
searchInput: string;
|
||||
setSearchInput: ( _text: string ) => void;
|
||||
tabs: Tab[],
|
||||
currentTabId: TAB_ID;
|
||||
projects: Object[],
|
||||
isLoading: boolean;
|
||||
memberId?: number;
|
||||
hasPermissions: boolean | undefined;
|
||||
requestPermissions: () => void;
|
||||
}
|
||||
|
||||
const Projects = ( {
|
||||
searchInput, setSearchInput, tabs, currentTabId, projects, isLoading, memberId
|
||||
}: Props ): Node => {
|
||||
searchInput,
|
||||
setSearchInput,
|
||||
tabs, currentTabId, projects, isLoading, memberId,
|
||||
hasPermissions,
|
||||
requestPermissions
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const navigation = useNavigation( );
|
||||
|
||||
@@ -86,7 +93,7 @@ const Projects = ( {
|
||||
}
|
||||
|
||||
if ( searchInput.length === 0 ) {
|
||||
if ( currentTabId === "JOINED" && !memberId ) {
|
||||
if ( currentTabId === TAB_ID.JOINED && !memberId ) {
|
||||
return (
|
||||
<View className="items-center">
|
||||
<Body1>{t( "You-havent-joined-any-projects-yet" )}</Body1>
|
||||
@@ -103,6 +110,36 @@ const Projects = ( {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
} as const;
|
||||
|
||||
const renderList = ( ) => {
|
||||
// hasPermission undefined means we haven't checked for location permissions yet
|
||||
// false means the user has denied or not yet given location permissions
|
||||
if ( currentTabId === TAB_ID.NEARBY && hasPermissions === false ) {
|
||||
return (
|
||||
<View className="flex-1 justify-center p-4">
|
||||
<View className="items-center">
|
||||
<Body2>{t( "To-view-nearby-projects-please-enable-location" )}</Body2>
|
||||
</View>
|
||||
<Button
|
||||
className="mt-5"
|
||||
text={t( "ALLOW-LOCATION-ACCESS" )}
|
||||
accessibilityHint={t( "Opens-location-permission-prompt" )}
|
||||
level="focus"
|
||||
onPress={( ) => requestPermissions()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={projects?.length === 0 && emptyListStyles}
|
||||
data={projects}
|
||||
renderItem={renderProject}
|
||||
testID="Project.list"
|
||||
ListEmptyComponent={renderEmptyList}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -122,13 +159,7 @@ const Projects = ( {
|
||||
<View className="mb-3" />
|
||||
</>
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={projects?.length === 0 && emptyListStyles}
|
||||
data={projects}
|
||||
renderItem={renderProject}
|
||||
testID="Project.list"
|
||||
ListEmptyComponent={renderEmptyList}
|
||||
/>
|
||||
{renderList( )}
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
import { searchProjects } from "api/projects";
|
||||
import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate";
|
||||
import _ from "lodash";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
useAuthenticatedQuery,
|
||||
@@ -9,24 +7,30 @@ import {
|
||||
useTranslation,
|
||||
useUserLocation
|
||||
} from "sharedHooks";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
|
||||
|
||||
import Projects from "./Projects";
|
||||
|
||||
const JOINED_TAB_ID = "JOINED";
|
||||
const FEATURED_TAB_ID = "FEATURED";
|
||||
const NEARBY_TAB_ID = "NEARBY";
|
||||
export enum TAB_ID {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
JOINED = "JOINED",
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
FEATURED = "FEATURED",
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
NEARBY = "NEARBY"
|
||||
}
|
||||
|
||||
const ProjectsContainer = ( ): Node => {
|
||||
const ProjectsContainer = ( ) => {
|
||||
const [searchInput, setSearchInput] = useState( "" );
|
||||
const currentUser = useCurrentUser( );
|
||||
const memberId = currentUser?.id;
|
||||
const { t } = useTranslation( );
|
||||
const [apiParams, setApiParams] = useState( { } );
|
||||
const [currentTabId, setCurrentTabId] = useState( JOINED_TAB_ID );
|
||||
const [permissionsGranted, setPermissionsGranted] = useState( false );
|
||||
const [currentTabId, setCurrentTabId] = useState( TAB_ID.JOINED );
|
||||
const { hasPermissions, renderPermissionsGate, requestPermissions } = useLocationPermission();
|
||||
const { userLocation } = useUserLocation( {
|
||||
skipName: true,
|
||||
permissionsGranted
|
||||
permissionsGranted: hasPermissions
|
||||
} );
|
||||
|
||||
const {
|
||||
@@ -41,11 +45,11 @@ const ProjectsContainer = ( ): Node => {
|
||||
);
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( currentTabId === JOINED_TAB_ID ) {
|
||||
if ( currentTabId === TAB_ID.JOINED ) {
|
||||
setApiParams( { member_id: memberId } );
|
||||
} else if ( currentTabId === FEATURED_TAB_ID ) {
|
||||
} else if ( currentTabId === TAB_ID.FEATURED ) {
|
||||
setApiParams( { featured: true } );
|
||||
} else if ( currentTabId === NEARBY_TAB_ID && userLocation ) {
|
||||
} else if ( currentTabId === TAB_ID.NEARBY && userLocation ) {
|
||||
setApiParams( {
|
||||
lat: userLocation.latitude,
|
||||
lng: userLocation.longitude
|
||||
@@ -66,24 +70,24 @@ const ProjectsContainer = ( ): Node => {
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: JOINED_TAB_ID,
|
||||
id: TAB_ID.JOINED,
|
||||
text: t( "JOINED" ),
|
||||
onPress: () => {
|
||||
setCurrentTabId( JOINED_TAB_ID );
|
||||
setCurrentTabId( TAB_ID.JOINED );
|
||||
}
|
||||
},
|
||||
{
|
||||
id: FEATURED_TAB_ID,
|
||||
id: TAB_ID.FEATURED,
|
||||
text: t( "FEATURED" ),
|
||||
onPress: () => {
|
||||
setCurrentTabId( FEATURED_TAB_ID );
|
||||
setCurrentTabId( TAB_ID.FEATURED );
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NEARBY_TAB_ID,
|
||||
id: TAB_ID.NEARBY,
|
||||
text: t( "NEARBY" ),
|
||||
onPress: () => {
|
||||
setCurrentTabId( NEARBY_TAB_ID );
|
||||
setCurrentTabId( TAB_ID.NEARBY );
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -102,14 +106,10 @@ const ProjectsContainer = ( ): Node => {
|
||||
projects={projects}
|
||||
isLoading={isLoading}
|
||||
memberId={memberId}
|
||||
hasPermissions={hasPermissions}
|
||||
requestPermissions={requestPermissions}
|
||||
/>
|
||||
<LocationPermissionGate
|
||||
permissionNeeded={currentTabId === NEARBY_TAB_ID}
|
||||
withoutNavigation
|
||||
onPermissionGranted={( ) => setPermissionsGranted( true )}
|
||||
onPermissionDenied={( ) => setPermissionsGranted( false )}
|
||||
onPermissionBlocked={( ) => setPermissionsGranted( false )}
|
||||
/>
|
||||
{renderPermissionsGate()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
// @flow
|
||||
|
||||
import PermissionGateContainer, {
|
||||
LOCATION_PERMISSIONS
|
||||
} from "components/SharedComponents/PermissionGateContainer";
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
type Props = {
|
||||
children?: Node,
|
||||
permissionNeeded?: boolean,
|
||||
onModalHide?: Function,
|
||||
onPermissionGranted?: Function,
|
||||
onPermissionDenied?: Function,
|
||||
onPermissionBlocked?: Function,
|
||||
withoutNavigation?: boolean
|
||||
};
|
||||
interface Props extends PropsWithChildren {
|
||||
permissionNeeded?: boolean;
|
||||
onModalHide?: () => void;
|
||||
onPermissionBlocked?: () => void;
|
||||
onPermissionDenied?: () => void;
|
||||
onPermissionGranted?: () => void;
|
||||
withoutNavigation?: boolean;
|
||||
}
|
||||
|
||||
const LocationPermissionGate = ( {
|
||||
children,
|
||||
@@ -25,12 +21,13 @@ const LocationPermissionGate = ( {
|
||||
onPermissionDenied,
|
||||
onPermissionBlocked,
|
||||
withoutNavigation
|
||||
}: Props ): Node => (
|
||||
}: Props ) => (
|
||||
<PermissionGateContainer
|
||||
permissions={LOCATION_PERMISSIONS}
|
||||
title={t( "Get-more-accurate-suggestions-create-useful-data" )}
|
||||
titleDenied={t( "Please-allow-Location-Access" )}
|
||||
body={t( "iNaturalist-uses-your-location-to-give-you" )}
|
||||
body2={t( "Youre-always-in-control-of-the-location-privacy" )}
|
||||
blockedPrompt={t( "Youve-previously-denied-location-permissions" )}
|
||||
buttonText={t( "USE-LOCATION" )}
|
||||
icon="map-marker-outline"
|
||||
@@ -11,25 +11,40 @@ interface Props {
|
||||
currentLocationButtonClassName?: string;
|
||||
handlePress: () => void;
|
||||
showCurrentLocationButton?: boolean;
|
||||
hasPermissions: boolean | undefined;
|
||||
renderPermissionsGate: () => React.JSX.Element;
|
||||
requestPermissions: () => void;
|
||||
}
|
||||
|
||||
const CurrentLocationButton = ( {
|
||||
currentLocationButtonClassName,
|
||||
handlePress,
|
||||
showCurrentLocationButton
|
||||
showCurrentLocationButton,
|
||||
hasPermissions,
|
||||
renderPermissionsGate,
|
||||
requestPermissions
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const onPress = ( ) => {
|
||||
if ( !hasPermissions ) {
|
||||
requestPermissions( );
|
||||
}
|
||||
handlePress( );
|
||||
};
|
||||
return showCurrentLocationButton && (
|
||||
<INatIconButton
|
||||
icon="location-crosshairs"
|
||||
className={classnames(
|
||||
"absolute bottom-5 right-5 bg-white rounded-full",
|
||||
currentLocationButtonClassName
|
||||
)}
|
||||
style={DROP_SHADOW}
|
||||
accessibilityLabel={t( "Zoom-to-current-location" )}
|
||||
onPress={handlePress}
|
||||
/>
|
||||
<>
|
||||
<INatIconButton
|
||||
icon="location-crosshairs"
|
||||
className={classnames(
|
||||
"absolute bottom-5 right-5 bg-white rounded-full",
|
||||
currentLocationButtonClassName
|
||||
)}
|
||||
style={DROP_SHADOW}
|
||||
accessibilityLabel={t( "Zoom-to-current-location" )}
|
||||
onPress={onPress}
|
||||
/>
|
||||
{renderPermissionsGate( )}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -111,7 +111,6 @@ const DetailsMap = ( {
|
||||
obsLatitude={latitude}
|
||||
obsLongitude={longitude}
|
||||
obscured={obscured}
|
||||
permissionRequested={false}
|
||||
positionalAccuracy={positionalAccuracy}
|
||||
region={region}
|
||||
showCurrentLocationButton
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import classnames from "classnames";
|
||||
import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate";
|
||||
import { View } from "components/styledComponents";
|
||||
import React, {
|
||||
useCallback,
|
||||
@@ -14,6 +13,7 @@ import MapView, {
|
||||
BoundingBox, LatLng, MapType, Region
|
||||
} from "react-native-maps";
|
||||
import { useDeviceOrientation } from "sharedHooks";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
|
||||
|
||||
import CurrentLocationButton from "./CurrentLocationButton";
|
||||
import {
|
||||
@@ -46,14 +46,10 @@ interface Props {
|
||||
onCurrentLocationPress?: () => void;
|
||||
onMapReady?: () => void;
|
||||
onPanDrag?: () => void;
|
||||
onPermissionBlocked?: () => void;
|
||||
onPermissionDenied?: () => void;
|
||||
onPermissionGranted?: () => void;
|
||||
onRegionChangeComplete?: ( _r: Region, _b: BoundingBox | undefined ) => void;
|
||||
onZoomChange?: ( _z: number ) => void;
|
||||
onZoomToNearby?: Function;
|
||||
openMapScreen?: () => void;
|
||||
permissionRequested?: boolean;
|
||||
positionalAccuracy?: number;
|
||||
region?: Region;
|
||||
scrollEnabled?: boolean;
|
||||
@@ -90,14 +86,10 @@ const Map = ( {
|
||||
onCurrentLocationPress,
|
||||
onMapReady = ( ) => undefined,
|
||||
onPanDrag = ( ) => undefined,
|
||||
onPermissionBlocked: onPermissionBlockedProp,
|
||||
onPermissionDenied: onPermissionDeniedProp,
|
||||
onPermissionGranted: onPermissionGrantedProp,
|
||||
onRegionChangeComplete,
|
||||
onZoomChange,
|
||||
onZoomToNearby,
|
||||
openMapScreen,
|
||||
permissionRequested: permissionRequestedProp,
|
||||
positionalAccuracy,
|
||||
region,
|
||||
scrollEnabled = true,
|
||||
@@ -123,8 +115,7 @@ const Map = ( {
|
||||
: 5
|
||||
);
|
||||
const navigation = useNavigation( );
|
||||
const [permissionRequested, setPermissionRequested] = useState( permissionRequestedProp );
|
||||
const [showsUserLocation, setShowsUserLocation] = useState( false );
|
||||
const { hasPermissions, renderPermissionsGate, requestPermissions } = useLocationPermission( );
|
||||
const [userLocation, setUserLocation] = useState<{
|
||||
accuracy: number;
|
||||
latitude: number;
|
||||
@@ -158,14 +149,6 @@ const Map = ( {
|
||||
startAtNearby
|
||||
);
|
||||
|
||||
// Prop kind of functions as a signal. Would make more sense if it was
|
||||
// declarative and not reactive, but hey, it's React
|
||||
useEffect( ( ) => {
|
||||
if ( permissionRequestedProp && permissionRequested === null ) {
|
||||
setPermissionRequested( true );
|
||||
}
|
||||
}, [permissionRequestedProp, permissionRequested] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( startAtNearby && zoomToNearbyRequested === null ) {
|
||||
setZoomToNearbyRequested( true );
|
||||
@@ -225,45 +208,15 @@ const Map = ( {
|
||||
zoomToNearbyRequested
|
||||
] );
|
||||
|
||||
// Kludge for the fact that the onUserLocationChange callback in MapView
|
||||
// won't fire if showsUserLocation is true on the first render
|
||||
useEffect( ( ) => {
|
||||
setShowsUserLocation( true );
|
||||
}, [] );
|
||||
|
||||
// PermissionGate callbacks need to use useCallback, otherwise they'll
|
||||
// trigger re-renders if/when they change
|
||||
// TODO: Johannes: I don't know if this is necessary anymore
|
||||
const onPermissionGranted = useCallback( ( ) => {
|
||||
if ( typeof ( onPermissionGrantedProp ) === "function" ) onPermissionGrantedProp( );
|
||||
setPermissionRequested( false );
|
||||
setShowsUserLocation( true );
|
||||
if ( startAtNearby ) {
|
||||
setZoomToNearbyRequested( true );
|
||||
}
|
||||
}, [
|
||||
onPermissionGrantedProp,
|
||||
setPermissionRequested,
|
||||
setZoomToNearbyRequested,
|
||||
startAtNearby
|
||||
] );
|
||||
const onPermissionBlocked = useCallback( ( ) => {
|
||||
if ( typeof ( onPermissionBlockedProp ) === "function" ) onPermissionBlockedProp( );
|
||||
setPermissionRequested( false );
|
||||
setShowsUserLocation( false );
|
||||
}, [
|
||||
onPermissionBlockedProp,
|
||||
setPermissionRequested,
|
||||
setShowsUserLocation
|
||||
] );
|
||||
const onPermissionDenied = useCallback( ( ) => {
|
||||
if ( typeof ( onPermissionDeniedProp ) === "function" ) onPermissionDeniedProp( );
|
||||
setPermissionRequested( false );
|
||||
setShowsUserLocation( false );
|
||||
}, [
|
||||
onPermissionDeniedProp,
|
||||
setPermissionRequested,
|
||||
setShowsUserLocation
|
||||
] );
|
||||
|
||||
const params = useMemo( ( ) => {
|
||||
const newTileParams = { ...tileMapParams };
|
||||
@@ -296,6 +249,10 @@ const Map = ( {
|
||||
}
|
||||
}, [currentZoom, onZoomChange] );
|
||||
|
||||
const renderCurrentLocationPermissionsGate = () => renderPermissionsGate( {
|
||||
onPermissionGranted
|
||||
} );
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -320,7 +277,7 @@ const Map = ( {
|
||||
const coordinate = locationChangeEvent?.nativeEvent?.coordinate;
|
||||
setUserLocation( coordinate );
|
||||
}}
|
||||
showsUserLocation={showsUserLocation}
|
||||
showsUserLocation={hasPermissions}
|
||||
showsMyLocationButton={false}
|
||||
loadingEnabled
|
||||
onRegionChangeComplete={async newRegion => {
|
||||
@@ -368,11 +325,13 @@ const Map = ( {
|
||||
<CurrentLocationButton
|
||||
showCurrentLocationButton={showCurrentLocationButton}
|
||||
currentLocationButtonClassName={currentLocationButtonClassName}
|
||||
hasPermissions={hasPermissions}
|
||||
renderPermissionsGate={renderCurrentLocationPermissionsGate}
|
||||
requestPermissions={requestPermissions}
|
||||
onPermissionGranted={onPermissionGranted}
|
||||
handlePress={( ) => {
|
||||
if ( onCurrentLocationPress ) { onCurrentLocationPress( ); }
|
||||
setZoomToUserLocationRequested( true );
|
||||
setShowsUserLocation( true );
|
||||
setPermissionRequested( true );
|
||||
}}
|
||||
/>
|
||||
<SwitchMapTypeButton
|
||||
@@ -382,13 +341,6 @@ const Map = ( {
|
||||
showSwitchMapTypeButton={showSwitchMapTypeButton}
|
||||
switchMapTypeButtonClassName={switchMapTypeButtonClassName}
|
||||
/>
|
||||
<LocationPermissionGate
|
||||
permissionNeeded={permissionRequested}
|
||||
onPermissionGranted={onPermissionGranted}
|
||||
onPermissionBlocked={onPermissionBlocked}
|
||||
onPermissionDenied={onPermissionDenied}
|
||||
withoutNavigation
|
||||
/>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -18,16 +18,31 @@ import {
|
||||
StatusBar
|
||||
} from "react-native";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
import { RESULTS } from "react-native-permissions";
|
||||
import { PermissionStatus, RESULTS } from "react-native-permissions";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
const BACKGROUND_IMAGE_STYLE = {
|
||||
opacity: 0.33,
|
||||
backgroundColor: "black"
|
||||
};
|
||||
} as const;
|
||||
|
||||
const isTablet = DeviceInfo.isTablet();
|
||||
|
||||
interface Props {
|
||||
requestPermission: () => void;
|
||||
grantStatus: PermissionStatus;
|
||||
icon: string;
|
||||
title?: string;
|
||||
titleDenied?: string;
|
||||
body?: string;
|
||||
body2?: string;
|
||||
blockedPrompt?: string;
|
||||
buttonText?: string;
|
||||
image?: number;
|
||||
onClose: () => void;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const PermissionGate = ( {
|
||||
requestPermission,
|
||||
grantStatus,
|
||||
@@ -35,12 +50,13 @@ const PermissionGate = ( {
|
||||
title = t( "Grant-Permission-title" ),
|
||||
titleDenied = t( "Please-Grant-Permission" ),
|
||||
body,
|
||||
body2,
|
||||
blockedPrompt = t( "Youve-denied-permission-prompt" ),
|
||||
buttonText = t( "GRANT-PERMISSION" ),
|
||||
image = require( "images/bart-zimny-W5XTTLpk1-I-unsplash.jpg" ),
|
||||
onClose,
|
||||
testID
|
||||
} ) => (
|
||||
}: Props ) => (
|
||||
<ViewWrapper wrapperClassName="bg-black" testID={testID}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="black" />
|
||||
<ImageBackground
|
||||
@@ -82,13 +98,16 @@ const PermissionGate = ( {
|
||||
/>
|
||||
) }
|
||||
<Heading2 className="text-center text-white mt-8 mb-5">
|
||||
{ grantStatus === null
|
||||
? title
|
||||
: titleDenied}
|
||||
{ grantStatus === RESULTS.BLOCKED
|
||||
? titleDenied
|
||||
: title}
|
||||
</Heading2>
|
||||
{ body && (
|
||||
<Body2 className="text-center text-white">{ body }</Body2>
|
||||
) }
|
||||
{ body2 && (
|
||||
<Body2 className="text-center text-white mt-5">{ body2 }</Body2>
|
||||
) }
|
||||
{ grantStatus === RESULTS.BLOCKED && (
|
||||
<Body2 className="text-center text-white mt-5">
|
||||
{ blockedPrompt }
|
||||
@@ -1,13 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import Modal from "components/SharedComponents/Modal.tsx";
|
||||
import _ from "lodash";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import {
|
||||
checkMultiple, PERMISSIONS, requestMultiple, RESULTS
|
||||
AndroidPermission,
|
||||
checkMultiple,
|
||||
Permission,
|
||||
PERMISSIONS,
|
||||
PermissionStatus,
|
||||
requestMultiple,
|
||||
RESULTS
|
||||
} from "react-native-permissions";
|
||||
|
||||
import PermissionGate from "./PermissionGate";
|
||||
@@ -15,61 +18,70 @@ import PermissionGate from "./PermissionGate";
|
||||
const usesAndroid10Permissions = Platform.OS === "android" && Platform.Version <= 29;
|
||||
const usesAndroid13Permissions = Platform.OS === "android" && Platform.Version >= 33;
|
||||
|
||||
let androidReadPermissions = [
|
||||
let androidReadWritePermissions: AndroidPermission[] = [
|
||||
PERMISSIONS.ANDROID.ACCESS_MEDIA_LOCATION
|
||||
];
|
||||
if ( usesAndroid10Permissions ) {
|
||||
androidReadPermissions = [...androidReadPermissions, PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE];
|
||||
androidReadWritePermissions = [
|
||||
...androidReadWritePermissions,
|
||||
PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE
|
||||
];
|
||||
} else if ( usesAndroid13Permissions ) {
|
||||
androidReadPermissions = [...androidReadPermissions, PERMISSIONS.ANDROID.READ_MEDIA_IMAGES];
|
||||
androidReadWritePermissions = [
|
||||
...androidReadWritePermissions,
|
||||
PERMISSIONS.ANDROID.READ_MEDIA_IMAGES
|
||||
];
|
||||
} else {
|
||||
androidReadPermissions = [...androidReadPermissions, PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE];
|
||||
androidReadWritePermissions = [
|
||||
...androidReadWritePermissions,
|
||||
PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE
|
||||
];
|
||||
}
|
||||
|
||||
const androidCameraPermissions = usesAndroid10Permissions
|
||||
? [PERMISSIONS.ANDROID.CAMERA, PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE]
|
||||
: [PERMISSIONS.ANDROID.CAMERA];
|
||||
|
||||
export const CAMERA_PERMISSIONS: Array<string> = Platform.OS === "ios"
|
||||
export const CAMERA_PERMISSIONS = Platform.OS === "ios"
|
||||
? [PERMISSIONS.IOS.CAMERA]
|
||||
: androidCameraPermissions;
|
||||
|
||||
export const AUDIO_PERMISSIONS: Array<string> = Platform.OS === "ios"
|
||||
export const AUDIO_PERMISSIONS = Platform.OS === "ios"
|
||||
? [PERMISSIONS.IOS.MICROPHONE]
|
||||
: [...androidReadPermissions, PERMISSIONS.ANDROID.RECORD_AUDIO];
|
||||
: [...androidReadWritePermissions, PERMISSIONS.ANDROID.RECORD_AUDIO];
|
||||
|
||||
export const READ_MEDIA_PERMISSIONS: Array<string> = Platform.OS === "ios"
|
||||
export const READ_WRITE_MEDIA_PERMISSIONS = Platform.OS === "ios"
|
||||
? [PERMISSIONS.IOS.PHOTO_LIBRARY]
|
||||
: androidReadPermissions;
|
||||
: androidReadWritePermissions;
|
||||
|
||||
export const WRITE_MEDIA_PERMISSIONS: Array<string> = Platform.OS === "ios"
|
||||
? [PERMISSIONS.IOS.PHOTO_LIBRARY_ADD_ONLY]
|
||||
: androidReadPermissions;
|
||||
|
||||
export const LOCATION_PERMISSIONS: Array<string> = Platform.OS === "ios"
|
||||
export const LOCATION_PERMISSIONS = Platform.OS === "ios"
|
||||
? [PERMISSIONS.IOS.LOCATION_WHEN_IN_USE]
|
||||
: [PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION];
|
||||
|
||||
type Props = {
|
||||
blockedPrompt?: string,
|
||||
body?: string,
|
||||
buttonText?: string,
|
||||
children?: Node,
|
||||
icon?: string,
|
||||
image?: Object,
|
||||
onModalHide?: Function,
|
||||
onPermissionBlocked?: Function,
|
||||
onPermissionDenied?: Function,
|
||||
onPermissionGranted?: Function,
|
||||
permissionNeeded?: boolean,
|
||||
permissions: Array<string>,
|
||||
testID?: string,
|
||||
title?: string,
|
||||
titleDenied: string,
|
||||
withoutNavigation?: boolean
|
||||
blockedPrompt?: string;
|
||||
body?: string;
|
||||
body2?: string;
|
||||
buttonText?: string;
|
||||
children?: React.ReactNode,
|
||||
icon: string;
|
||||
image?: number;
|
||||
onModalHide?: () => void;
|
||||
onPermissionBlocked?: () => void;
|
||||
onPermissionDenied?: () => void;
|
||||
onPermissionGranted?: () => void;
|
||||
permissionNeeded?: boolean;
|
||||
permissions: Permission[];
|
||||
testID?: string;
|
||||
title?: string;
|
||||
titleDenied?: string;
|
||||
withoutNavigation?: boolean;
|
||||
};
|
||||
|
||||
export function permissionResultFromMultiple( multiResults: Object ): string {
|
||||
interface MultiResult {
|
||||
[permission: string]: PermissionStatus;
|
||||
}
|
||||
export function permissionResultFromMultiple( multiResults: MultiResult ) {
|
||||
if ( typeof ( multiResults ) !== "object" ) {
|
||||
throw new Error(
|
||||
"permissionResultFromMultiple received something other than an object. "
|
||||
@@ -85,6 +97,7 @@ export function permissionResultFromMultiple( multiResults: Object ): string {
|
||||
if ( _.find( multiResults, ( permResult, _perm ) => permResult === RESULTS.UNAVAILABLE ) ) {
|
||||
return RESULTS.UNAVAILABLE;
|
||||
}
|
||||
// Note: we're not checking for RESULTS.LIMITED here and treat it as GRANTED
|
||||
return RESULTS.GRANTED;
|
||||
}
|
||||
|
||||
@@ -96,6 +109,7 @@ export function permissionResultFromMultiple( multiResults: Object ): string {
|
||||
const PermissionGateContainer = ( {
|
||||
blockedPrompt,
|
||||
body,
|
||||
body2,
|
||||
buttonText,
|
||||
children,
|
||||
icon,
|
||||
@@ -111,8 +125,8 @@ const PermissionGateContainer = ( {
|
||||
title,
|
||||
titleDenied,
|
||||
withoutNavigation
|
||||
}: Props ): Node => {
|
||||
const [result, setResult] = useState( null );
|
||||
}: Props ) => {
|
||||
const [result, setResult] = useState<PermissionStatus | null>( null );
|
||||
const [modalShown, setModalShown] = useState( false );
|
||||
|
||||
const navigation = useNavigation();
|
||||
@@ -217,6 +231,7 @@ const PermissionGateContainer = ( {
|
||||
title={title}
|
||||
titleDenied={titleDenied}
|
||||
body={body}
|
||||
body2={body2}
|
||||
blockedPrompt={blockedPrompt}
|
||||
buttonText={buttonText}
|
||||
image={image}
|
||||
@@ -1,8 +1,6 @@
|
||||
// @flow
|
||||
import { fontRegular } from "appConstants/fontFamilies.ts";
|
||||
import { INatIcon, INatIconButton } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { TextInput, useTheme } from "react-native-paper";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
@@ -11,17 +9,17 @@ import colors from "styles/tailwindColors";
|
||||
|
||||
const DROP_SHADOW = getShadowForColor( colors.darkGray );
|
||||
|
||||
type Props = {
|
||||
autoFocus?: boolean,
|
||||
clearSearch?: Function,
|
||||
containerClass?: string,
|
||||
handleTextChange: Function,
|
||||
hasShadow?: boolean,
|
||||
// $FlowIgnore
|
||||
input?: unknown,
|
||||
placeholder?: string,
|
||||
testID?: string,
|
||||
value: string,
|
||||
interface Props {
|
||||
autoFocus?: boolean;
|
||||
clearSearch?: () => void;
|
||||
containerClass?: string;
|
||||
handleTextChange: ( _text: string ) => void;
|
||||
hasShadow?: boolean;
|
||||
// TODO: check react-native-paper docs for correct type
|
||||
input?: any,
|
||||
placeholder?: string;
|
||||
testID?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// Ensure this component is placed outside of scroll views
|
||||
@@ -36,7 +34,7 @@ const SearchBar = ( {
|
||||
placeholder,
|
||||
testID,
|
||||
value
|
||||
}: Props ): Node => {
|
||||
}: Props ) => {
|
||||
const theme = useTheme( );
|
||||
const { t } = useTranslation( );
|
||||
|
||||
@@ -44,7 +42,7 @@ const SearchBar = ( {
|
||||
borderColor: "lightgray",
|
||||
borderRadius: 8,
|
||||
borderWidth: 1
|
||||
};
|
||||
} as const;
|
||||
|
||||
const style = {
|
||||
...( hasShadow
|
||||
@@ -53,7 +51,7 @@ const SearchBar = ( {
|
||||
fontSize: 16,
|
||||
lineHeight: 18,
|
||||
paddingRight: 28
|
||||
};
|
||||
} as const;
|
||||
|
||||
// kind of tricky to change the font here:
|
||||
// https://github.com/callstack/react-native-paper/issues/3615#issuecomment-1402025033
|
||||
@@ -65,7 +63,7 @@ const SearchBar = ( {
|
||||
fontFamily: fontRegular
|
||||
}
|
||||
}
|
||||
};
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<View className={containerClass}>
|
||||
@@ -5,7 +5,7 @@ import React from "react";
|
||||
import { GestureResponderEvent, TouchableOpacity } from "react-native";
|
||||
import useTranslation from "sharedHooks/useTranslation";
|
||||
|
||||
interface Tab {
|
||||
export interface Tab {
|
||||
id: string;
|
||||
text: string;
|
||||
testID?: string;
|
||||
|
||||
@@ -27,7 +27,6 @@ export { default as InlineUser } from "./InlineUser/InlineUser";
|
||||
export { default as InputField } from "./InputField";
|
||||
export { default as KebabMenu } from "./KebabMenu";
|
||||
export { default as KeyboardDismissableView } from "./KeyboardDismissableView";
|
||||
export { default as LocationPermissionGate } from "./LocationPermissionGate";
|
||||
export { default as DetailsMap } from "./Map/DetailsMap";
|
||||
export { default as Map } from "./Map/Map";
|
||||
export { default as MediaNavButtons } from "./MediaNavButtons";
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
// @flow
|
||||
|
||||
import classnames from "classnames";
|
||||
import {
|
||||
Image, Pressable, View
|
||||
} from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
type Props = {
|
||||
photoUris: Array<string>,
|
||||
selectedPhotoUri: string,
|
||||
onPressPhoto: Function
|
||||
};
|
||||
interface Props {
|
||||
photoUris: string[];
|
||||
selectedPhotoUri: string;
|
||||
onPressPhoto: ( _uri: string ) => void;
|
||||
}
|
||||
|
||||
const ObsPhotoSelectionList = ( {
|
||||
photoUris, selectedPhotoUri, onPressPhoto
|
||||
}: Props ): Node => {
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const renderPhoto = useCallback( ( { item } ) => (
|
||||
@@ -36,7 +33,7 @@ const ObsPhotoSelectionList = ( {
|
||||
className={classnames(
|
||||
"rounded-lg overflow-hidden",
|
||||
{
|
||||
"border border-inatGreen border-[3px]": selectedPhotoUri === item
|
||||
"border-inatGreen border-[3px]": selectedPhotoUri === item
|
||||
}
|
||||
)}
|
||||
testID={`ObsPhotoSelectionList.border.${item}`}
|
||||
@@ -24,8 +24,8 @@ type Props = {
|
||||
photoUris: Array<string>,
|
||||
reloadSuggestions: Function,
|
||||
selectedPhotoUri: string,
|
||||
// setLocationPermissionNeeded: Function,
|
||||
// showImproveWithLocationButton: boolean,
|
||||
improveWithLocationButtonOnPress: () => void;
|
||||
showImproveWithLocationButton: boolean;
|
||||
suggestions: Object
|
||||
};
|
||||
|
||||
@@ -36,8 +36,8 @@ const Suggestions = ( {
|
||||
photoUris,
|
||||
reloadSuggestions,
|
||||
selectedPhotoUri,
|
||||
// setLocationPermissionNeeded,
|
||||
// showImproveWithLocationButton,
|
||||
improveWithLocationButtonOnPress,
|
||||
showImproveWithLocationButton,
|
||||
suggestions
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
@@ -86,16 +86,16 @@ const Suggestions = ( {
|
||||
reloadSuggestions={reloadSuggestions}
|
||||
selectedPhotoUri={selectedPhotoUri}
|
||||
suggestions={suggestions}
|
||||
// setLocationPermissionNeeded={setLocationPermissionNeeded}
|
||||
// showImproveWithLocationButton={showImproveWithLocationButton}
|
||||
improveWithLocationButtonOnPress={improveWithLocationButtonOnPress}
|
||||
showImproveWithLocationButton={showImproveWithLocationButton}
|
||||
/>
|
||||
), [
|
||||
onPressPhoto,
|
||||
photoUris,
|
||||
reloadSuggestions,
|
||||
selectedPhotoUri,
|
||||
// setLocationPermissionNeeded,
|
||||
// showImproveWithLocationButton,
|
||||
improveWithLocationButtonOnPress,
|
||||
showImproveWithLocationButton,
|
||||
suggestions
|
||||
] );
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import MediaViewerModal from "components/MediaViewer/MediaViewerModal";
|
||||
// import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate";
|
||||
import _ from "lodash";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -11,6 +9,7 @@ import ObservationPhoto from "realmModels/ObservationPhoto";
|
||||
import {
|
||||
useIsConnected
|
||||
} from "sharedHooks";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
|
||||
// import { log } from "sharedHelpers/logger";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
@@ -31,7 +30,7 @@ const initialSuggestions = {
|
||||
isLoading: true
|
||||
};
|
||||
|
||||
const SuggestionsContainer = ( ): Node => {
|
||||
const SuggestionsContainer = ( ) => {
|
||||
const isOnline = useIsConnected( );
|
||||
// clearing the cache of resized images for the score_image API
|
||||
// placing this here means we can keep the app size small
|
||||
@@ -49,9 +48,13 @@ const SuggestionsContainer = ( ): Node => {
|
||||
...initialSuggestions,
|
||||
showSuggestionsWithLocation: evidenceHasLocation
|
||||
} );
|
||||
// const [locationPermissionNeeded, setLocationPermissionNeeded] = useState( false );
|
||||
const { hasPermissions, renderPermissionsGate, requestPermissions } = useLocationPermission( );
|
||||
const showImproveWithLocationButton = hasPermissions === false;
|
||||
// const showImproveWithLocationButton = !evidenceHasLocation
|
||||
// && params?.lastScreen === "CameraWithDevice";
|
||||
const improveWithLocationButtonOnPress = useCallback( ( ) => {
|
||||
requestPermissions( );
|
||||
}, [requestPermissions] );
|
||||
|
||||
const {
|
||||
showSuggestionsWithLocation,
|
||||
@@ -232,8 +235,8 @@ const SuggestionsContainer = ( ): Node => {
|
||||
photoUris={photoUris}
|
||||
reloadSuggestions={reloadSuggestions}
|
||||
selectedPhotoUri={selectedPhotoUri}
|
||||
// setLocationPermissionNeeded={setLocationPermissionNeeded}
|
||||
// showImproveWithLocationButton={showImproveWithLocationButton}
|
||||
improveWithLocationButtonOnPress={improveWithLocationButtonOnPress}
|
||||
showImproveWithLocationButton={showImproveWithLocationButton}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
<MediaViewerModal
|
||||
@@ -242,13 +245,7 @@ const SuggestionsContainer = ( ): Node => {
|
||||
uri={selectedPhotoUri}
|
||||
photos={innerPhotos}
|
||||
/>
|
||||
{/* <LocationPermissionGate
|
||||
permissionNeeded={locationPermissionNeeded}
|
||||
withoutNavigation
|
||||
onPermissionGranted={( ) => console.log( "permission granted" )}
|
||||
onPermissionDenied={( ) => console.log( "permission denied" )}
|
||||
onPermissionBlocked={( ) => console.log( "permission blocked" )}
|
||||
/> */}
|
||||
{renderPermissionsGate()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@ import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import {
|
||||
Body2,
|
||||
Body3,
|
||||
Button,
|
||||
INatIcon,
|
||||
INatIconButton
|
||||
} from "components/SharedComponents";
|
||||
import { Pressable, View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useTheme } from "react-native-paper";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
@@ -15,21 +15,26 @@ import AddCommentPrompt from "./AddCommentPrompt";
|
||||
import CommentBox from "./CommentBox";
|
||||
import ObsPhotoSelectionList from "./ObsPhotoSelectionList";
|
||||
|
||||
type Props = {
|
||||
onPressPhoto: Function,
|
||||
photoUris: Array<string>,
|
||||
reloadSuggestions: Function,
|
||||
selectedPhotoUri: string,
|
||||
suggestions: Object
|
||||
};
|
||||
interface Props {
|
||||
onPressPhoto: ( _uri: string ) => void;
|
||||
photoUris: string[];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
reloadSuggestions: ( { showLocation }: { showLocation: boolean } ) => void;
|
||||
selectedPhotoUri: string;
|
||||
suggestions: Object;
|
||||
improveWithLocationButtonOnPress: () => void;
|
||||
showImproveWithLocationButton: boolean;
|
||||
}
|
||||
|
||||
const SuggestionsHeader = ( {
|
||||
onPressPhoto,
|
||||
photoUris,
|
||||
reloadSuggestions,
|
||||
selectedPhotoUri,
|
||||
suggestions
|
||||
}: Props ): Node => {
|
||||
suggestions,
|
||||
improveWithLocationButtonOnPress,
|
||||
showImproveWithLocationButton
|
||||
}: Props ) => {
|
||||
const { t } = useTranslation( );
|
||||
const navigation = useNavigation( );
|
||||
const { params } = useRoute( );
|
||||
@@ -68,16 +73,16 @@ const SuggestionsHeader = ( {
|
||||
onPressPhoto={onPressPhoto}
|
||||
/>
|
||||
</View>
|
||||
{/* {showImproveWithLocationButton && (
|
||||
{showImproveWithLocationButton && (
|
||||
<View className="mx-5 mt-5">
|
||||
<Button
|
||||
text={t( "IMPROVE-THESE-SUGGESTIONS-BY-USING-YOUR-LOCATION" )}
|
||||
accessibilityHint={t( "Opens-location-permission-prompt" )}
|
||||
level="focus"
|
||||
onPress={( ) => setLocationPermissionNeeded( true )}
|
||||
onPress={( ) => improveWithLocationButtonOnPress()}
|
||||
/>
|
||||
</View>
|
||||
)} */}
|
||||
)}
|
||||
{showOfflineText && (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
|
||||
@@ -56,7 +56,6 @@ const TaxonMapPreview = ( {
|
||||
mapHeight={230}
|
||||
mapViewClassName="-mx-3"
|
||||
openMapScreen={() => setShowMapModal( true )}
|
||||
permissionRequested={false}
|
||||
region={region}
|
||||
scrollEnabled={false}
|
||||
tileMapParams={obsParams}
|
||||
|
||||
@@ -76,6 +76,7 @@ All-observations = All observations
|
||||
All-organisms = All organisms
|
||||
# As in intellectual property rights over a photo or other creative work
|
||||
all-rights-reserved = all rights reserved
|
||||
ALLOW-LOCATION-ACCESS = ALLOW LOCATION ACCESS
|
||||
# As in automated identification suggestions
|
||||
Almost-done = Almost done!
|
||||
Already-have-an-account = Already have an account? Log in
|
||||
@@ -471,7 +472,7 @@ INATURALIST-STORE = INATURALIST STORE
|
||||
INATURALIST-TEAM = INATURALIST TEAM
|
||||
iNaturalist-users-who-have-left-an-identification = iNaturalist users who have left an identification on another user's observation
|
||||
iNaturalist-users-who-have-observed = iNaturalist users who have observed a particular taxon at a particular time and place
|
||||
iNaturalist-uses-your-location-to-give-you = iNaturalist uses your location to give you better identification suggestions and we can automatically add a location to your observations, which helps scientists. We also use it to help you find organisms observed near your location. You’re always in control of the location privacy of every observation you create.
|
||||
iNaturalist-uses-your-location-to-give-you = iNaturalist uses your location to give you better identification suggestions and we can automatically add a location to your observations, which helps scientists. We also use it to help you find organisms observed near your location.
|
||||
iNaturalists-apps-are-designed-and-developed = iNaturalist's apps are designed, developed, and supported by the iNaturalist team: Yaron Budowski, Amanda Bullington, Tony Iwane, Johannes Klein, Patrick Leary, Scott Loarie, Abhas Misraraj, Sylvain Morin, Carrie Seltzer, Alex Shepard, Angie Ta, Ken-ichi Ueda, Jason Walthall, & Jane Weeden.
|
||||
iNaturalists-vision-is-a-world = iNaturalist's vision is a world where everyone can understand and sustain biodiversity through the practice of observing wild organisms and sharing information about them.
|
||||
Individual-encounters-with-organisms = Individual encounters with organisms at a particular time and location, usually with evidence
|
||||
@@ -788,7 +789,6 @@ Removes-your-vote-of-agreement = Removes your vote of agreement
|
||||
Removes-your-vote-of-disagreement = Removes your vote of disagreement
|
||||
# Quality grade option
|
||||
Research-Grade = Research Grade
|
||||
Reset = Reset
|
||||
# Reset password button
|
||||
RESET-PASSWORD = RESET PASSWORD
|
||||
# Label for a button that resets a sound recording
|
||||
@@ -827,7 +827,6 @@ Scientific-Name-Common-Name = Scientific Name (Common Name)
|
||||
SEARCH = SEARCH
|
||||
# Title for a search interface
|
||||
Search = Search
|
||||
SEARCH-FOR-A-LOCATION = SEARCH FOR A LOCATION
|
||||
Search-for-a-project = Search for a project
|
||||
SEARCH-FOR-A-TAXON = SEARCH FOR A TAXON
|
||||
Search-for-a-taxon = Search for a taxon
|
||||
@@ -950,6 +949,8 @@ This-organism-was-placed-by-humans = This organism was placed in this location b
|
||||
To-access-all-other-settings = To access all other account settings, click here:
|
||||
To-learn-more-about-what-information = To learn more about what information we collect and how we use it, please see our Privacy Policy and our Terms of Use.
|
||||
To-sync-your-observations-to-iNaturalist = To sync your observations to iNaturalist, please log in.
|
||||
To-view-nearby-organisms-please-enable-location = To view nearby organisms, please enable location.
|
||||
To-view-nearby-projects-please-enable-location = To view nearby projects, please enable location.
|
||||
Toggle-map-type = Toggle map type
|
||||
TOP-ID-SUGGESTION = TOP ID SUGGESTION
|
||||
Traditional-Project = Traditional Project
|
||||
@@ -1157,6 +1158,7 @@ Your-donation-to-iNaturalist = Your donation to iNaturalist supports the improve
|
||||
Your-email-is-confirmed = Your email is confirmed! Please log in to continue.
|
||||
Your-identification-will-be-posted-with-the-following-comment = Your identification will be posted with the following comment:
|
||||
Your-location-uncertainty-is-over-x-km = Your location uncertainty is over { $x } km, 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.
|
||||
Youre-always-in-control-of-the-location-privacy = You’re always in control of the location privacy of every observation you create.
|
||||
# Text prompting the user to open Settings to grant permission after
|
||||
# permission has been denied
|
||||
Youve-denied-permission-prompt = You’ve denied permission. Please grant permission in the settings app.
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"comment": "As in intellectual property rights over a photo or other creative work",
|
||||
"val": "all rights reserved"
|
||||
},
|
||||
"ALLOW-LOCATION-ACCESS": "ALLOW LOCATION ACCESS",
|
||||
"Almost-done": {
|
||||
"comment": "As in automated identification suggestions",
|
||||
"val": "Almost done!"
|
||||
@@ -632,7 +633,7 @@
|
||||
"INATURALIST-TEAM": "INATURALIST TEAM",
|
||||
"iNaturalist-users-who-have-left-an-identification": "iNaturalist users who have left an identification on another user's observation",
|
||||
"iNaturalist-users-who-have-observed": "iNaturalist users who have observed a particular taxon at a particular time and place",
|
||||
"iNaturalist-uses-your-location-to-give-you": "iNaturalist uses your location to give you better identification suggestions and we can automatically add a location to your observations, which helps scientists. We also use it to help you find organisms observed near your location. You’re always in control of the location privacy of every observation you create.",
|
||||
"iNaturalist-uses-your-location-to-give-you": "iNaturalist uses your location to give you better identification suggestions and we can automatically add a location to your observations, which helps scientists. We also use it to help you find organisms observed near your location.",
|
||||
"iNaturalists-apps-are-designed-and-developed": "iNaturalist's apps are designed, developed, and supported by the iNaturalist team: Yaron Budowski, Amanda Bullington, Tony Iwane, Johannes Klein, Patrick Leary, Scott Loarie, Abhas Misraraj, Sylvain Morin, Carrie Seltzer, Alex Shepard, Angie Ta, Ken-ichi Ueda, Jason Walthall, & Jane Weeden.",
|
||||
"iNaturalists-vision-is-a-world": "iNaturalist's vision is a world where everyone can understand and sustain biodiversity through the practice of observing wild organisms and sharing information about them.",
|
||||
"Individual-encounters-with-organisms": "Individual encounters with organisms at a particular time and location, usually with evidence",
|
||||
@@ -1072,7 +1073,6 @@
|
||||
"comment": "Quality grade option",
|
||||
"val": "Research Grade"
|
||||
},
|
||||
"Reset": "Reset",
|
||||
"RESET-PASSWORD": {
|
||||
"comment": "Reset password button",
|
||||
"val": "RESET PASSWORD"
|
||||
@@ -1130,7 +1130,6 @@
|
||||
"comment": "Title for a search interface",
|
||||
"val": "Search"
|
||||
},
|
||||
"SEARCH-FOR-A-LOCATION": "SEARCH FOR A LOCATION",
|
||||
"Search-for-a-project": "Search for a project",
|
||||
"SEARCH-FOR-A-TAXON": "SEARCH FOR A TAXON",
|
||||
"Search-for-a-taxon": "Search for a taxon",
|
||||
@@ -1287,6 +1286,8 @@
|
||||
"To-access-all-other-settings": "To access all other account settings, click here:",
|
||||
"To-learn-more-about-what-information": "To learn more about what information we collect and how we use it, please see our Privacy Policy and our Terms of Use.",
|
||||
"To-sync-your-observations-to-iNaturalist": "To sync your observations to iNaturalist, please log in.",
|
||||
"To-view-nearby-organisms-please-enable-location": "To view nearby organisms, please enable location.",
|
||||
"To-view-nearby-projects-please-enable-location": "To view nearby projects, please enable location.",
|
||||
"Toggle-map-type": "Toggle map type",
|
||||
"TOP-ID-SUGGESTION": "TOP ID SUGGESTION",
|
||||
"Traditional-Project": "Traditional Project",
|
||||
@@ -1453,6 +1454,7 @@
|
||||
"Your-email-is-confirmed": "Your email is confirmed! Please log in to continue.",
|
||||
"Your-identification-will-be-posted-with-the-following-comment": "Your identification will be posted with the following comment:",
|
||||
"Your-location-uncertainty-is-over-x-km": "Your location uncertainty is over { $x } km, 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.",
|
||||
"Youre-always-in-control-of-the-location-privacy": "You’re always in control of the location privacy of every observation you create.",
|
||||
"Youve-denied-permission-prompt": {
|
||||
"comment": "Text prompting the user to open Settings to grant permission after\npermission has been denied",
|
||||
"val": "You’ve denied permission. Please grant permission in the settings app."
|
||||
|
||||
@@ -76,6 +76,7 @@ All-observations = All observations
|
||||
All-organisms = All organisms
|
||||
# As in intellectual property rights over a photo or other creative work
|
||||
all-rights-reserved = all rights reserved
|
||||
ALLOW-LOCATION-ACCESS = ALLOW LOCATION ACCESS
|
||||
# As in automated identification suggestions
|
||||
Almost-done = Almost done!
|
||||
Already-have-an-account = Already have an account? Log in
|
||||
@@ -471,7 +472,7 @@ INATURALIST-STORE = INATURALIST STORE
|
||||
INATURALIST-TEAM = INATURALIST TEAM
|
||||
iNaturalist-users-who-have-left-an-identification = iNaturalist users who have left an identification on another user's observation
|
||||
iNaturalist-users-who-have-observed = iNaturalist users who have observed a particular taxon at a particular time and place
|
||||
iNaturalist-uses-your-location-to-give-you = iNaturalist uses your location to give you better identification suggestions and we can automatically add a location to your observations, which helps scientists. We also use it to help you find organisms observed near your location. You’re always in control of the location privacy of every observation you create.
|
||||
iNaturalist-uses-your-location-to-give-you = iNaturalist uses your location to give you better identification suggestions and we can automatically add a location to your observations, which helps scientists. We also use it to help you find organisms observed near your location.
|
||||
iNaturalists-apps-are-designed-and-developed = iNaturalist's apps are designed, developed, and supported by the iNaturalist team: Yaron Budowski, Amanda Bullington, Tony Iwane, Johannes Klein, Patrick Leary, Scott Loarie, Abhas Misraraj, Sylvain Morin, Carrie Seltzer, Alex Shepard, Angie Ta, Ken-ichi Ueda, Jason Walthall, & Jane Weeden.
|
||||
iNaturalists-vision-is-a-world = iNaturalist's vision is a world where everyone can understand and sustain biodiversity through the practice of observing wild organisms and sharing information about them.
|
||||
Individual-encounters-with-organisms = Individual encounters with organisms at a particular time and location, usually with evidence
|
||||
@@ -788,7 +789,6 @@ Removes-your-vote-of-agreement = Removes your vote of agreement
|
||||
Removes-your-vote-of-disagreement = Removes your vote of disagreement
|
||||
# Quality grade option
|
||||
Research-Grade = Research Grade
|
||||
Reset = Reset
|
||||
# Reset password button
|
||||
RESET-PASSWORD = RESET PASSWORD
|
||||
# Label for a button that resets a sound recording
|
||||
@@ -827,7 +827,6 @@ Scientific-Name-Common-Name = Scientific Name (Common Name)
|
||||
SEARCH = SEARCH
|
||||
# Title for a search interface
|
||||
Search = Search
|
||||
SEARCH-FOR-A-LOCATION = SEARCH FOR A LOCATION
|
||||
Search-for-a-project = Search for a project
|
||||
SEARCH-FOR-A-TAXON = SEARCH FOR A TAXON
|
||||
Search-for-a-taxon = Search for a taxon
|
||||
@@ -950,6 +949,8 @@ This-organism-was-placed-by-humans = This organism was placed in this location b
|
||||
To-access-all-other-settings = To access all other account settings, click here:
|
||||
To-learn-more-about-what-information = To learn more about what information we collect and how we use it, please see our Privacy Policy and our Terms of Use.
|
||||
To-sync-your-observations-to-iNaturalist = To sync your observations to iNaturalist, please log in.
|
||||
To-view-nearby-organisms-please-enable-location = To view nearby organisms, please enable location.
|
||||
To-view-nearby-projects-please-enable-location = To view nearby projects, please enable location.
|
||||
Toggle-map-type = Toggle map type
|
||||
TOP-ID-SUGGESTION = TOP ID SUGGESTION
|
||||
Traditional-Project = Traditional Project
|
||||
@@ -1157,6 +1158,7 @@ Your-donation-to-iNaturalist = Your donation to iNaturalist supports the improve
|
||||
Your-email-is-confirmed = Your email is confirmed! Please log in to continue.
|
||||
Your-identification-will-be-posted-with-the-following-comment = Your identification will be posted with the following comment:
|
||||
Your-location-uncertainty-is-over-x-km = Your location uncertainty is over { $x } km, 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.
|
||||
Youre-always-in-control-of-the-location-privacy = You’re always in control of the location privacy of every observation you create.
|
||||
# Text prompting the user to open Settings to grant permission after
|
||||
# permission has been denied
|
||||
Youve-denied-permission-prompt = You’ve denied permission. Please grant permission in the settings app.
|
||||
|
||||
@@ -9,8 +9,9 @@ import { Heading4 } from "components/SharedComponents";
|
||||
import Mortal from "components/SharedComponents/Mortal";
|
||||
import PermissionGateContainer, {
|
||||
AUDIO_PERMISSIONS,
|
||||
CAMERA_PERMISSIONS
|
||||
} from "components/SharedComponents/PermissionGateContainer";
|
||||
CAMERA_PERMISSIONS,
|
||||
READ_WRITE_MEDIA_PERMISSIONS
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
import SoundRecorder from "components/SoundRecorder/SoundRecorder";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
@@ -69,6 +70,21 @@ const CameraContainerWithPermission = ( ) => (
|
||||
</PermissionGateContainer>
|
||||
);
|
||||
|
||||
const GalleryContainerWithPermission = ( ) => (
|
||||
<PermissionGateContainer
|
||||
permissions={READ_WRITE_MEDIA_PERMISSIONS}
|
||||
title={t( "Observe-and-identify-organisms-from-your-gallery" )}
|
||||
titleDenied={t( "Please-Allow-Gallery-Access" )}
|
||||
body={t( "Upload-photos-from-your-gallery-and-create-observations" )}
|
||||
blockedPrompt={t( "Youve-previously-denied-gallery-permissions" )}
|
||||
buttonText={t( "CHOOSE-PHOTOS" )}
|
||||
icon="gallery"
|
||||
image={require( "images/viviana-rishe-j2330n6bg3I-unsplash.jpg" )}
|
||||
>
|
||||
<PhotoGallery />
|
||||
</PermissionGateContainer>
|
||||
);
|
||||
|
||||
const SoundRecorderWithPermission = ( ) => (
|
||||
<Mortal>
|
||||
<PermissionGateContainer
|
||||
@@ -105,7 +121,7 @@ const NoBottomTabStackNavigator = ( ): Node => (
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PhotoGallery"
|
||||
component={PhotoGallery}
|
||||
component={GalleryContainerWithPermission}
|
||||
options={hideHeader}
|
||||
/>
|
||||
<Stack.Screen
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-shadow */
|
||||
import { t } from "i18next";
|
||||
import { isEqual } from "lodash";
|
||||
import * as React from "react";
|
||||
import { LatLng } from "react-native-maps";
|
||||
@@ -30,6 +29,10 @@ export enum EXPLORE_ACTION {
|
||||
SET_MEDIA = "SET_MEDIA",
|
||||
SET_PHOTO_LICENSE = "SET_PHOTO_LICENSE",
|
||||
SET_PLACE = "SET_PLACE",
|
||||
SET_PLACE_MODE_NEARBY = "SET_PLACE_MODE_NEARBY",
|
||||
SET_PLACE_MODE_WORLDWIDE = "SET_PLACE_MODE_WORLDWIDE",
|
||||
SET_PLACE_MODE_MAP_AREA = "SET_PLACE_MODE_MAP_AREA",
|
||||
SET_PLACE_MODE_PLACE = "SET_PLACE_MODE_PLACE",
|
||||
SET_PROJECT = "SET_PROJECT",
|
||||
SET_REVIEWED = "SET_REVIEWED",
|
||||
SET_TAXON_NAME = "SET_TAXON_NAME",
|
||||
@@ -142,12 +145,18 @@ export enum PHOTO_LICENSE {
|
||||
CCBYNCND = "CCBYNCND",
|
||||
}
|
||||
|
||||
export enum PLACE_MODE {
|
||||
NEARBY = "NEARBY",
|
||||
WORLDWIDE = "WORLDWIDE",
|
||||
PLACE = "PLACE",
|
||||
MAP_AREA = "MAP_AREA"
|
||||
}
|
||||
|
||||
interface MapBoundaries {
|
||||
swlat?: LatLng["latitude"],
|
||||
swlng?: LatLng["longitude"],
|
||||
nelat?: LatLng["latitude"],
|
||||
nelng?: LatLng["longitude"],
|
||||
place_guess: string
|
||||
swlat?: LatLng["latitude"];
|
||||
swlng?: LatLng["longitude"];
|
||||
nelat?: LatLng["latitude"];
|
||||
nelng?: LatLng["longitude"];
|
||||
}
|
||||
|
||||
interface PLACE {
|
||||
@@ -160,6 +169,13 @@ interface PLACE {
|
||||
type: string
|
||||
}
|
||||
|
||||
interface DefaultLocation {
|
||||
placeMode: PLACE_MODE,
|
||||
lat?: number,
|
||||
lng?: number,
|
||||
radius?: number
|
||||
}
|
||||
|
||||
type ExploreProviderProps = {children: React.ReactNode}
|
||||
type State = {
|
||||
casual: boolean,
|
||||
@@ -185,6 +201,7 @@ type State = {
|
||||
photoLicense: PHOTO_LICENSE,
|
||||
place: PLACE | null | undefined,
|
||||
place_guess: string,
|
||||
placeMode: PLACE_MODE,
|
||||
place_id: number | null | undefined,
|
||||
// TODO: technically this is not any Object but a "Project"
|
||||
// and should be typed as such (e.g., in realm model)
|
||||
@@ -217,17 +234,21 @@ type Action = {type: EXPLORE_ACTION.RESET}
|
||||
storedState: State
|
||||
}
|
||||
| { type: EXPLORE_ACTION.FILTER_BY_ICONIC_TAXON_UNKNOWN }
|
||||
| {type: EXPLORE_ACTION.SET_EXPLORE_LOCATION, exploreLocation: Object}
|
||||
| {type: EXPLORE_ACTION.SET_EXPLORE_LOCATION, exploreLocation: DefaultLocation}
|
||||
| {
|
||||
type: EXPLORE_ACTION.SET_PLACE,
|
||||
place: PLACE,
|
||||
placeId: number,
|
||||
placeGuess: string,
|
||||
placeGuess?: string,
|
||||
lat: number,
|
||||
lng: number,
|
||||
radius: number,
|
||||
storedState: State
|
||||
}
|
||||
| {type: EXPLORE_ACTION.SET_PLACE_MODE_NEARBY}
|
||||
| {type: EXPLORE_ACTION.SET_PLACE_MODE_WORLDWIDE}
|
||||
| {type: EXPLORE_ACTION.SET_PLACE_MODE_MAP_AREA}
|
||||
| {type: EXPLORE_ACTION.SET_PLACE_MODE_PLACE}
|
||||
| {type: EXPLORE_ACTION.SET_PROJECT, project: Object, projectId: number, storedState: State}
|
||||
| {type: EXPLORE_ACTION.CHANGE_SORT_BY, sortBy: SORT_BY}
|
||||
| {type: EXPLORE_ACTION.TOGGLE_RESEARCH_GRADE}
|
||||
@@ -260,12 +281,7 @@ const ExploreContext = React.createContext<
|
||||
isNotInitialState: boolean;
|
||||
discardChanges(): void;
|
||||
numberOfFilters: number;
|
||||
defaultExploreLocation(): Promise<{
|
||||
place_guess: string;
|
||||
lat?: number;
|
||||
lng?: number;
|
||||
radius?: number;
|
||||
}>;
|
||||
defaultExploreLocation(): Promise<DefaultLocation>;
|
||||
} | undefined>( undefined );
|
||||
|
||||
// Every key in this object represents a numbered filter in the UI
|
||||
@@ -304,7 +320,12 @@ const defaultFilters = {
|
||||
|
||||
const initialState: State = {
|
||||
...defaultFilters,
|
||||
// The very first time the user opens the explore screen, we want the place mode to be
|
||||
// nearby (even if we don't have location permission yet and not actually make any API request)
|
||||
placeMode: PLACE_MODE.NEARBY,
|
||||
place: undefined,
|
||||
// String to display as the human-readable place name, not to be used for default explore search
|
||||
// modes, like Nearby or Worldwide
|
||||
place_guess: "",
|
||||
place_id: undefined,
|
||||
return_bounds: true,
|
||||
@@ -319,15 +340,15 @@ function isValidDateFormat( date: string ): boolean {
|
||||
return regex.test( date );
|
||||
}
|
||||
|
||||
async function defaultExploreLocation( ) {
|
||||
async function defaultExploreLocation( ): Promise<DefaultLocation> {
|
||||
const location = await fetchUserLocation( );
|
||||
if ( !location || !location.latitude ) {
|
||||
return {
|
||||
place_guess: t( "Worldwide" )
|
||||
placeMode: PLACE_MODE.WORLDWIDE
|
||||
};
|
||||
}
|
||||
return {
|
||||
place_guess: t( "Nearby" ),
|
||||
placeMode: PLACE_MODE.NEARBY,
|
||||
lat: location?.latitude,
|
||||
lng: location?.longitude,
|
||||
radius: 50
|
||||
@@ -339,10 +360,12 @@ async function defaultExploreLocation( ) {
|
||||
// state
|
||||
function exploreReducer( state: State, action: Action ) {
|
||||
switch ( action.type ) {
|
||||
// Reset the state to the initial state, but place mode
|
||||
// should be set to worldwide no matter if location and not nearby
|
||||
case EXPLORE_ACTION.RESET:
|
||||
return {
|
||||
...initialState,
|
||||
...action.exploreLocation
|
||||
placeMode: PLACE_MODE.WORLDWIDE
|
||||
};
|
||||
case EXPLORE_ACTION.DISCARD:
|
||||
return action.snapshot;
|
||||
@@ -381,6 +404,27 @@ function exploreReducer( state: State, action: Action ) {
|
||||
...state,
|
||||
...action.exploreLocation
|
||||
};
|
||||
case EXPLORE_ACTION.SET_PLACE_MODE_NEARBY:
|
||||
return {
|
||||
...state,
|
||||
placeMode: PLACE_MODE.NEARBY
|
||||
|
||||
};
|
||||
case EXPLORE_ACTION.SET_PLACE_MODE_WORLDWIDE:
|
||||
return {
|
||||
...state,
|
||||
placeMode: PLACE_MODE.WORLDWIDE
|
||||
};
|
||||
case EXPLORE_ACTION.SET_PLACE_MODE_MAP_AREA:
|
||||
return {
|
||||
...state,
|
||||
placeMode: PLACE_MODE.MAP_AREA
|
||||
};
|
||||
case EXPLORE_ACTION.SET_PLACE_MODE_PLACE:
|
||||
return {
|
||||
...state,
|
||||
placeMode: PLACE_MODE.PLACE
|
||||
};
|
||||
case EXPLORE_ACTION.SET_PLACE:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -5,8 +5,7 @@ class LocalPreferences extends Realm.Object {
|
||||
name: "LocalPreferences",
|
||||
properties: {
|
||||
last_deleted_sync_time: "date?",
|
||||
last_sync_time: "date?",
|
||||
explore_location_permission_shown: { type: "bool", default: false }
|
||||
last_sync_time: "date?"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,13 +30,21 @@ export default {
|
||||
User,
|
||||
Vote
|
||||
],
|
||||
schemaVersion: 51,
|
||||
schemaVersion: 52,
|
||||
path: `${RNFS.DocumentDirectoryPath}/db.realm`,
|
||||
// https://github.com/realm/realm-js/pull/6076 embedded constraints
|
||||
migrationOptions: {
|
||||
resolveEmbeddedConstraints: true
|
||||
},
|
||||
migration: ( oldRealm, newRealm ) => {
|
||||
if ( oldRealm.schemaVersion < 52 ) {
|
||||
const oldPrefs = oldRealm.objects( "LocalPreferences" );
|
||||
const newPrefs = newRealm.objects( "LocalPreferences" );
|
||||
oldPrefs.keys( ).forEach( objectIndex => {
|
||||
const newObsSound = newPrefs[objectIndex];
|
||||
delete newObsSound.explore_location_permission_shown;
|
||||
} );
|
||||
}
|
||||
if ( oldRealm.schemaVersion < 51 ) {
|
||||
// const oldIdentifications = oldRealm.objects( "Identification" );
|
||||
// const newIdentifications = newRealm.objects( "Identification" );
|
||||
|
||||
@@ -6,7 +6,7 @@ type UserLocation = {
|
||||
}
|
||||
const fetchUserLocation = async ( ): Promise<UserLocation> => new Promise( resolve => {
|
||||
setTimeout( ( ) => resolve( {
|
||||
// Chuck's house. Note that the e2e tests run in a UTC environment, so the
|
||||
// Darwin's house. Note that the e2e tests run in a UTC environment, so the
|
||||
// observed_on_string will be set to a UTC time. If these coordinates
|
||||
// fall within a time zone west of that (i.e. in the past),
|
||||
// observation creation will fail during the period of the day when UTC
|
||||
|
||||
@@ -2,11 +2,10 @@ import Geolocation, { GeolocationResponse } from "@react-native-community/geoloc
|
||||
import {
|
||||
LOCATION_PERMISSIONS,
|
||||
permissionResultFromMultiple
|
||||
} from "components/SharedComponents/PermissionGateContainer";
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
import { Platform } from "react-native";
|
||||
import {
|
||||
checkMultiple,
|
||||
Permission,
|
||||
RESULTS
|
||||
} from "react-native-permissions";
|
||||
|
||||
@@ -30,9 +29,7 @@ interface UserLocation {
|
||||
|
||||
const fetchUserLocation = async ( ): Promise<UserLocation | null> => {
|
||||
const permissionResult = permissionResultFromMultiple(
|
||||
// TODO if/when we convert PermissionGateContainer to typescript, this
|
||||
// case should be unnecessary
|
||||
await checkMultiple( LOCATION_PERMISSIONS as Permission[] )
|
||||
await checkMultiple( LOCATION_PERMISSIONS )
|
||||
);
|
||||
|
||||
// TODO: handle case where iOS permissions are not granted
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import { galleryPhotosPath } from "appConstants/paths.ts";
|
||||
import {
|
||||
LOCATION_PERMISSIONS,
|
||||
permissionResultFromMultiple
|
||||
} from "components/SharedComponents/PermissionGateContainer";
|
||||
import {
|
||||
useEffect, useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import RNFS from "react-native-fs";
|
||||
import { checkMultiple, RESULTS } from "react-native-permissions";
|
||||
|
||||
// Please don't change this to an aliased path or the e2e mock will not get
|
||||
// used in our e2e tests on Github Actions
|
||||
@@ -29,6 +24,7 @@ const useCurrentObservationLocation = (
|
||||
mountedRef: unknown,
|
||||
currentObservation: Object,
|
||||
updateObservationKeys: Function,
|
||||
hasPermissions: boolean,
|
||||
options: Object = { }
|
||||
): Object => {
|
||||
const latitude = currentObservation?.latitude;
|
||||
@@ -64,7 +60,6 @@ const useCurrentObservationLocation = (
|
||||
const [fetchingLocation, setFetchingLocation] = useState( false );
|
||||
const [positionalAccuracy, setPositionalAccuracy] = useState( INITIAL_POSITIONAL_ACCURACY );
|
||||
const [lastLocationFetchTime, setLastLocationFetchTime] = useState( 0 );
|
||||
const [permissionResult, setPermissionResult] = useState( null );
|
||||
const [currentLocation, setCurrentLocation] = useState( null );
|
||||
|
||||
useEffect( () => {
|
||||
@@ -97,11 +92,7 @@ const useCurrentObservationLocation = (
|
||||
if ( !mountedRef.current ) return;
|
||||
if ( !shouldFetchLocation ) return;
|
||||
|
||||
const newPermissionResult = permissionResultFromMultiple(
|
||||
await checkMultiple( LOCATION_PERMISSIONS )
|
||||
);
|
||||
setPermissionResult( newPermissionResult );
|
||||
if ( newPermissionResult !== RESULTS.GRANTED ) {
|
||||
if ( !hasPermissions ) {
|
||||
setFetchingLocation( false );
|
||||
setShouldFetchLocation( false );
|
||||
return;
|
||||
@@ -155,10 +146,10 @@ const useCurrentObservationLocation = (
|
||||
}, [
|
||||
currentObservation,
|
||||
fetchingLocation,
|
||||
hasPermissions,
|
||||
lastLocationFetchTime,
|
||||
mountedRef,
|
||||
numLocationFetches,
|
||||
permissionResult,
|
||||
positionalAccuracy,
|
||||
setFetchingLocation,
|
||||
shouldFetchLocation,
|
||||
@@ -186,7 +177,6 @@ const useCurrentObservationLocation = (
|
||||
// location requests is in flight, but this tells the external consumer
|
||||
// whether the overall location fetching process is happening
|
||||
isFetchingLocation: shouldFetchLocation,
|
||||
permissionResult,
|
||||
numLocationFetches
|
||||
};
|
||||
};
|
||||
|
||||
108
src/sharedHooks/useLocationPermission.tsx
Normal file
108
src/sharedHooks/useLocationPermission.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate.tsx";
|
||||
import {
|
||||
LOCATION_PERMISSIONS,
|
||||
permissionResultFromMultiple
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { AppState } from "react-native";
|
||||
import {
|
||||
checkMultiple,
|
||||
RESULTS
|
||||
} from "react-native-permissions";
|
||||
|
||||
// PermissionGate callbacks need to use useCallback, otherwise they'll
|
||||
// trigger re-renders if/when they change
|
||||
interface LocationPermissionCallbacks {
|
||||
onPermissionGranted?: ( ) => void;
|
||||
onPermissionDenied?: ( ) => void;
|
||||
onPermissionBlocked?: ( ) => void;
|
||||
onModalHide?: ( ) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to check and request location permissions.
|
||||
* @returns {boolean} hasPermissions - Undefined if permissions have not been checked yet.
|
||||
* True if permissions have been granted. False if permissions have been denied.
|
||||
* @returns {Function} renderPermissionsGate - A function to render the permissions gate.
|
||||
* @returns {Function} requestPermissions - A function to request location permissions.
|
||||
* Essentially just a wrapper around toggling permissionNeeded for the LocationPermissionGate.
|
||||
*/
|
||||
const useLocationPermission = ( ) => {
|
||||
const [hasPermissions, setHasPermissions] = useState<boolean>( );
|
||||
const [showPermissionGate, setShowPermissionGate] = useState( false );
|
||||
|
||||
// PermissionGate callbacks need to use useCallback, otherwise they'll
|
||||
// trigger re-renders if/when they change
|
||||
const renderPermissionsGate = ( callbacks?: LocationPermissionCallbacks ) => {
|
||||
const {
|
||||
onPermissionGranted,
|
||||
onPermissionDenied,
|
||||
onPermissionBlocked,
|
||||
onModalHide
|
||||
} = callbacks || { };
|
||||
return (
|
||||
<LocationPermissionGate
|
||||
permissionNeeded={showPermissionGate}
|
||||
withoutNavigation
|
||||
onModalHide={( ) => {
|
||||
setShowPermissionGate( false );
|
||||
if ( onModalHide ) onModalHide( );
|
||||
}}
|
||||
onPermissionGranted={( ) => {
|
||||
setShowPermissionGate( false );
|
||||
setHasPermissions( true );
|
||||
if ( onPermissionGranted ) onPermissionGranted( );
|
||||
}}
|
||||
onPermissionDenied={( ) => {
|
||||
setShowPermissionGate( false );
|
||||
setHasPermissions( false );
|
||||
if ( onPermissionDenied ) onPermissionDenied( );
|
||||
}}
|
||||
onPermissionBlocked={( ) => {
|
||||
setShowPermissionGate( false );
|
||||
setHasPermissions( false );
|
||||
if ( onPermissionBlocked ) onPermissionBlocked( );
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function requestPermissions() {
|
||||
setShowPermissionGate( true );
|
||||
}
|
||||
|
||||
async function checkPermissions() {
|
||||
const permissionsResult = permissionResultFromMultiple(
|
||||
await checkMultiple( LOCATION_PERMISSIONS )
|
||||
);
|
||||
if ( permissionsResult === RESULTS.GRANTED ) {
|
||||
setHasPermissions( true );
|
||||
} else {
|
||||
setHasPermissions( false );
|
||||
console.log( "Location permissions have not been granted." );
|
||||
}
|
||||
}
|
||||
|
||||
// Check permissions every time the app is back in the foreground. The user could
|
||||
// have changed permissions in the settings app while the current screen was in the background.
|
||||
useEffect( () => {
|
||||
const subscription = AppState.addEventListener( "change", appState => {
|
||||
if ( appState === "active" ) {
|
||||
checkPermissions();
|
||||
}
|
||||
} );
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [] );
|
||||
|
||||
// Check permissions on mount
|
||||
useEffect( () => {
|
||||
checkPermissions();
|
||||
}, [] );
|
||||
|
||||
return { hasPermissions, renderPermissionsGate, requestPermissions };
|
||||
};
|
||||
|
||||
export default useLocationPermission;
|
||||
@@ -5,11 +5,10 @@ import Geolocation, {
|
||||
import {
|
||||
LOCATION_PERMISSIONS,
|
||||
permissionResultFromMultiple
|
||||
} from "components/SharedComponents/PermissionGateContainer";
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
checkMultiple,
|
||||
Permission,
|
||||
RESULTS
|
||||
} from "react-native-permissions";
|
||||
import fetchPlaceName from "sharedHelpers/fetchPlaceName";
|
||||
@@ -64,7 +63,7 @@ function useUserLocation(
|
||||
useEffect( ( ) => {
|
||||
async function checkPermissions() {
|
||||
const permissionsResult = permissionResultFromMultiple(
|
||||
await checkMultiple( LOCATION_PERMISSIONS as Permission[] )
|
||||
await checkMultiple( LOCATION_PERMISSIONS )
|
||||
);
|
||||
if ( permissionsResult === RESULTS.GRANTED ) {
|
||||
setPermissionsGranted( true );
|
||||
|
||||
@@ -86,11 +86,6 @@ describe( "ObsEdit offline", ( ) => {
|
||||
renderAppWithComponent(
|
||||
<ObsEdit />
|
||||
);
|
||||
await waitFor( ( ) => {
|
||||
expect(
|
||||
screen.getByTestId( "EvidenceSection.fetchingLocationIndicator" )
|
||||
).toBeTruthy( );
|
||||
} );
|
||||
await waitFor( ( ) => {
|
||||
expect( mockGetCurrentPosition ).toHaveBeenCalled( );
|
||||
}, { timeout: LOCATION_FETCH_INTERVAL * 2 } );
|
||||
|
||||
@@ -290,12 +290,8 @@ describe( "logged in", ( ) => {
|
||||
await navigateToRootExplore( );
|
||||
const speciesViewIcon = await screen.findByLabelText( /Species View/ );
|
||||
expect( speciesViewIcon ).toBeVisible( );
|
||||
const locationPermission = await screen.findByText( /Please allow Location Access/ );
|
||||
expect( locationPermission ).toBeVisible( );
|
||||
const closeButton = await screen.findByLabelText( /Close permission request screen/ );
|
||||
await actor.press( closeButton );
|
||||
const defaultGlobalLocation = await screen.findByText( /Worldwide/ );
|
||||
expect( defaultGlobalLocation ).toBeVisible( );
|
||||
const defaultNearbyLocationText = await screen.findByText( /Nearby/ );
|
||||
expect( defaultNearbyLocationText ).toBeVisible( );
|
||||
const backButton = screen.queryByTestId( "Explore.BackButton" );
|
||||
expect( backButton ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
@@ -12,9 +12,6 @@ jest.mock( "sharedHooks/useIsConnected", ( ) => ( {
|
||||
default: ( ) => true
|
||||
} ) );
|
||||
|
||||
// Don't need permission gates if we're just testing DetailsTab
|
||||
jest.mock( "components/SharedComponents/LocationPermissionGate", ( ) => "" );
|
||||
|
||||
// Before migrating to Jest 27 this line was:
|
||||
// jest.useFakeTimers();
|
||||
// TODO: replace with modern usage of jest.useFakeTimers
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen } from "@testing-library/react-native";
|
||||
import PermissionGate from "components/SharedComponents/PermissionGate";
|
||||
import PermissionGate from "components/SharedComponents/PermissionGate.tsx";
|
||||
import React from "react";
|
||||
import { RESULTS } from "react-native-permissions";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user