diff --git a/ios/Podfile b/ios/Podfile index 725f2b6ad..d2af0c616 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,7 +32,7 @@ setup_permissions([ # 'Motion', # 'Notifications', 'PhotoLibrary', - 'PhotoLibraryAddOnly', + # 'PhotoLibraryAddOnly', # 'Reminders', # 'Siri', # 'SpeechRecognition', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b69bf34d1..03d48408e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1477,12 +1477,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: d3f49c53809116a5d38da093a8aa78bf551aed09 BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 - DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 FasterImage: 60d0750ddbcefff0070c4c17309c2d1d6cc650f0 FBLazyVector: 9f533d5a4c75ca77c8ed774aced1a91a0701781e FBReactNativeSpec: 40b791f4a1df779e7e4aa12c000319f4f216d40a fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 MMKV: 36a22a9ec84c9bb960613a089ddf6f48be9312b0 @@ -1560,7 +1560,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: bc2cdb2dc42facdf34992ae364b8a728e19a3686 RNLocalize: e8694475db034bf601e17bd3dfa8986565e769eb - RNPermissions: b3d6efca086546e29a2920cd649a0ab04ca77794 + RNPermissions: a123c47480a5f5d7a04d40637ad1f7360a41b465 RNReanimated: 6cfa556540186ce7ae7a0b048f369236b1d86ebb RNScreens: b6b64d956af3715adbfe84808694ae82d3fec74f RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3 @@ -1572,6 +1572,6 @@ SPEC CHECKSUMS: VisionCameraPluginInatVision: 8480b3955bc608e913135d3bebaa57939911fb82 Yoga: c716aea2ee01df6258550c7505fa61b248145ced -PODFILE CHECKSUM: eebd76aa39f99b44754431ed68ce0cfbfc5ec2f7 +PODFILE CHECKSUM: ebb6b37cf92e00a96e3123d4db14c5658b4e5929 COCOAPODS: 1.15.2 diff --git a/ios/iNaturalistReactNative/Info.plist b/ios/iNaturalistReactNative/Info.plist index 3de8a7977..00b9eea28 100644 --- a/ios/iNaturalistReactNative/Info.plist +++ b/ios/iNaturalistReactNative/Info.plist @@ -54,7 +54,7 @@ NSAppleMusicUsageDescription Add existing photos and sounds to your observations. NSCameraUsageDescription - ${PRODUCT_NAME} uses the camera to add photos to your observations of nature. + iNaturalist Next uses the camera to add photos to your observations of nature. NSLocationAlwaysAndWhenInUseUsageDescription We do not intentionally request this permission. If you are seeing this, please take a screenshot and email it to help@inaturalist.org NSLocationWhenInUseUsageDescription @@ -62,9 +62,9 @@ NSMicrophoneUsageDescription Record sound observations of nature. NSPhotoLibraryAddUsageDescription - Export ${PRODUCT_NAME} photos to your library. + Export iNaturalist Next photos to your library. NSPhotoLibraryUsageDescription - Export and import ${PRODUCT_NAME} photos to and from your library. + Export and import iNaturalist Next photos to and from your library. UIAppFonts Lato-Bold.ttf diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js index e18789211..0549e927f 100644 --- a/src/components/Camera/AICamera/AICamera.js +++ b/src/components/Camera/AICamera/AICamera.js @@ -42,7 +42,7 @@ type Props = { camera: Object, device: Object, flipCamera: Function, - handleCheckmarkPress: Function, + handleCheckmarkPress: ( _result: any | null ) => void, isLandscapeMode: boolean }; diff --git a/src/components/Camera/AICamera/AICameraButtons.js b/src/components/Camera/AICamera/AICameraButtons.js index c55d86825..f87e138cb 100644 --- a/src/components/Camera/AICamera/AICameraButtons.js +++ b/src/components/Camera/AICamera/AICameraButtons.js @@ -3,7 +3,7 @@ import CameraFlip from "components/Camera/Buttons/CameraFlip"; import Close from "components/Camera/Buttons/Close"; import Flash from "components/Camera/Buttons/Flash"; -import TakePhoto from "components/Camera/Buttons/TakePhoto"; +import TakePhoto from "components/Camera/Buttons/TakePhoto.tsx"; import Zoom from "components/Camera/Buttons/Zoom"; import TabletButtons from "components/Camera/TabletButtons"; import { View } from "components/styledComponents"; @@ -60,7 +60,7 @@ type Props = { setNumStoredResults?: Function, showPrediction: boolean, showZoomButton: boolean, - takePhoto: Function, + takePhoto: () => Promise, takePhotoOptions: Object, toggleFlash: Function, zoomTextValue: string diff --git a/src/components/Camera/Buttons/TakePhoto.js b/src/components/Camera/Buttons/TakePhoto.tsx similarity index 91% rename from src/components/Camera/Buttons/TakePhoto.js rename to src/components/Camera/Buttons/TakePhoto.tsx index f8d08468a..6eaf06090 100644 --- a/src/components/Camera/Buttons/TakePhoto.js +++ b/src/components/Camera/Buttons/TakePhoto.tsx @@ -1,5 +1,3 @@ -// @flow - import classnames from "classnames"; import { INatIcon @@ -7,7 +5,6 @@ import { import { Pressable, View } from "components/styledComponents"; -import type { Node } from "react"; import React from "react"; import { useTheme } from "react-native-paper"; import { useTranslation } from "sharedHooks"; @@ -16,17 +13,17 @@ import colors from "styles/tailwindColors"; const DROP_SHADOW = getShadowForColor( colors.darkGray ); -type Props = { - takePhoto: Function, - disabled: boolean, - showPrediction?: boolean +interface Props { + takePhoto: () => Promise; + disabled: boolean; + showPrediction?: boolean; } const TakePhoto = ( { takePhoto, disabled, showPrediction -}: Props ): Node => { +}: Props ) => { const { t } = useTranslation( ); const theme = useTheme( ); diff --git a/src/components/Camera/CameraWithDevice.tsx b/src/components/Camera/CameraWithDevice.tsx index cac1655a1..7b0c0b0cd 100644 --- a/src/components/Camera/CameraWithDevice.tsx +++ b/src/components/Camera/CameraWithDevice.tsx @@ -1,11 +1,7 @@ import { useIsFocused, useNavigation } from "@react-navigation/native"; -import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate"; import PermissionGateContainer, { - LOCATION_PERMISSIONS, - permissionResultFromMultiple, - WRITE_MEDIA_PERMISSIONS -} - from "components/SharedComponents/PermissionGateContainer"; + READ_WRITE_MEDIA_PERMISSIONS +} from "components/SharedComponents/PermissionGateContainer.tsx"; import { View } from "components/styledComponents"; import React, { useCallback, useEffect, useRef, useState @@ -13,11 +9,6 @@ import React, { import { StatusBar } from "react-native"; import DeviceInfo from "react-native-device-info"; import Orientation from "react-native-orientation-locker"; -import { - checkMultiple, - Permission, - RESULTS -} from "react-native-permissions"; import { Camera, CameraDevice } from "react-native-vision-camera"; // import { log } from "sharedHelpers/logger"; import { useTranslation } from "sharedHooks"; @@ -25,6 +16,7 @@ import useDeviceOrientation, { LANDSCAPE_LEFT, LANDSCAPE_RIGHT } from "sharedHooks/useDeviceOrientation.ts"; +import useLocationPermission from "sharedHooks/useLocationPermission.tsx"; import AICamera from "./AICamera/AICamera"; import usePrepareStoreAndNavigate from "./hooks/usePrepareStoreAndNavigate"; @@ -69,7 +61,16 @@ const CameraWithDevice = ( { // permission gate on ObsEdit const [addPhotoPermissionGateWasClosed, setAddPhotoPermissionGateWasClosed] = useState( false ); const isFocused = useIsFocused( ); - const [locationPermissionGranted, setLocationPermissionGranted] = useState( false ); + + // Check if location permission granted b/c usePrepareStoreAndNavigate and + // useUserLocation need to know if permission has been granted to fetch the + // user's location while the camera is active. We don't want to *ask* for + // permission here b/c we want to avoid overloading a new user with + // permission requests and they will just have seen the camera permission + // request before landing here, so it's ok if we're not fetching the + // location here for the user's first observation (suggestions might be a + // bit off and we'll fetch the obs coordinates on ObsEdit) + const { hasPermissions } = useLocationPermission( ); // logger.debug( `isFocused: ${isFocused}` ); const prepareStoreAndNavigate = usePrepareStoreAndNavigate( { @@ -80,7 +81,7 @@ const CameraWithDevice = ( { // up and use that to pass along to Suggestions when the user navigates // there... but we only want to do that while the camera has focus and we // have permission - shouldFetchLocation: isFocused && locationPermissionGranted + shouldFetchLocation: isFocused && !!hasPermissions } ); const isLandscapeMode = [LANDSCAPE_LEFT, LANDSCAPE_RIGHT].includes( deviceOrientation ); @@ -154,66 +155,25 @@ const CameraWithDevice = ( { return unsubscribe; }, [navigation] ); - // Check if location permission granted b/c usePrepareStoreAndNavigate and - // useUserLocation need to know if permission has been granted to fetch the - // user's location while the camera is active. We don't want to *ask* for - // permission here b/c we want to avoid overloading a new user with - // permission requests and they will just have seen the camera permission - // request before landing here, so it's ok if we're not fetching the - // location here for the user's first observation (suggestions might be a - // bit off and we'll fetch the obs coordinates on ObsEdit) - useEffect( ( ) => { - async function checkLocationPermissions() { - const permissionsResult = permissionResultFromMultiple( - await checkMultiple( LOCATION_PERMISSIONS as Permission[] ) - ); - if ( permissionsResult === RESULTS.GRANTED ) { - setLocationPermissionGranted( true ); - } else { - console.warn( - "Location permissions have not been granted. You probably need to use a PermissionGate" - ); - } - } - checkLocationPermissions( ); - }, [] ); - return ( - {/* TODO why is this even here? The camera doesn't need location - permissions. Suggestions does. ~~~~kueda20240611 */} - {/* a weird quirk of react-native-modal is you can show subsequent modals - when a modal is nested in another modal. location permission is shown first - because the save photo modal pops up a second system alert on iOS asking - how much access to give */} - setAddPhotoPermissionGateWasClosed( true )} + onPermissionGranted={onPhotoPermissionGranted} + onPermissionDenied={onPhotoPermissionDenied} withoutNavigation - onPermissionGranted={( ) => { - // This probably doesn't do anything, but on the off chance we're - // able to grab coordinates immediately after the user grants - // permission, that will probably yield better suggestions on the - // next screen than nothing. - setLocationPermissionGranted( true ); - }} - > - setAddPhotoPermissionGateWasClosed( true )} - onPermissionGranted={onPhotoPermissionGranted} - onPermissionDenied={onPhotoPermissionDenied} - withoutNavigation - permissionNeeded={checkmarkTapped} - /> - + permissionNeeded={checkmarkTapped} + /> {cameraType === "Standard" ? ( Promise, } const CameraNavButtons = ( { diff --git a/src/components/Camera/StandardCamera/CameraOptionsButtons.js b/src/components/Camera/StandardCamera/CameraOptionsButtons.js index 0355cabd3..7fcc23f06 100644 --- a/src/components/Camera/StandardCamera/CameraOptionsButtons.js +++ b/src/components/Camera/StandardCamera/CameraOptionsButtons.js @@ -13,7 +13,7 @@ import Animated from "react-native-reanimated"; const isTablet = DeviceInfo.isTablet(); type Props = { - takePhoto: Function, + takePhoto: () => Promise, handleClose: Function, disabled: boolean, photosTaken: boolean, diff --git a/src/components/Camera/TabletButtons.js b/src/components/Camera/TabletButtons.js index 960444254..95d773338 100644 --- a/src/components/Camera/TabletButtons.js +++ b/src/components/Camera/TabletButtons.js @@ -47,7 +47,7 @@ type Props = { rotatableAnimatedStyle: Object, showPrediction?: boolean, showZoomButton: boolean, - takePhoto: Function, + takePhoto: () => Promise, takePhotoOptions: Object, toggleFlash: Function, zoomTextValue: string diff --git a/src/components/Explore/Explore.js b/src/components/Explore/Explore.js index b7f2bd919..ae4a9bd80 100644 --- a/src/components/Explore/Explore.js +++ b/src/components/Explore/Explore.js @@ -4,12 +4,15 @@ import { refresh } from "@react-native-community/netinfo"; import classnames from "classnames"; import ExploreFiltersModal from "components/Explore/Modals/ExploreFiltersModal"; import { + Body2, + Button, INatIconButton, OfflineNotice, RadioButtonSheet, ViewWrapper } from "components/SharedComponents"; import { View } from "components/styledComponents"; +import { PLACE_MODE } from "providers/ExploreContext.tsx"; import type { Node } from "react"; import React, { useState } from "react"; import { Alert } from "react-native"; @@ -56,7 +59,11 @@ type Props = { updateTaxon: Function, updateLocation: Function, updateUser: Function, - updateProject: Function + updateProject: Function, + // TODO: change to PLACE_MODE in Typescript + placeMode: string, + hasLocationPermissions: ?boolean, + requestLocationPermissions: Function } const Explore = ( { @@ -73,7 +80,10 @@ const Explore = ( { updateTaxon, updateLocation, updateUser, - updateProject + updateProject, + placeMode, + hasLocationPermissions, + requestLocationPermissions }: Props ): Node => { const theme = useTheme( ); const { t } = useTranslation( ); @@ -107,6 +117,69 @@ const Explore = ( { /> ); + const renderMainContent = ( ) => { + if ( !isOnline ) { + return ( + refresh()} + /> + ); + } + // hasLocationPermissions === undefined means we haven't checked for location permissions yet + if ( placeMode === PLACE_MODE.NEARBY && hasLocationPermissions === false ) { + return ( + + + {t( "To-view-nearby-organisms-please-enable-location" )} + +