diff --git a/android/app/src/main/assets/fonts/INatIcon.ttf b/android/app/src/main/assets/fonts/INatIcon.ttf index fbd695bec..9ae941596 100644 Binary files a/android/app/src/main/assets/fonts/INatIcon.ttf and b/android/app/src/main/assets/fonts/INatIcon.ttf differ diff --git a/android/link-assets-manifest.json b/android/link-assets-manifest.json index 8b83c37e9..c38428621 100644 --- a/android/link-assets-manifest.json +++ b/android/link-assets-manifest.json @@ -3,7 +3,7 @@ "data": [ { "path": "assets/fonts/INatIcon.ttf", - "sha1": "4f576448e7cb11aa54c32307a73fcc265ea4a920" + "sha1": "dd93213119594cc223f9e40f6a69df60dd5578d0" }, { "path": "assets/fonts/Lato-Bold.ttf", diff --git a/assets/fonts/INatIcon.ttf b/assets/fonts/INatIcon.ttf index fbd695bec..9ae941596 100644 Binary files a/assets/fonts/INatIcon.ttf and b/assets/fonts/INatIcon.ttf differ diff --git a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj index 3e7a5840a..424e140e5 100644 --- a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj +++ b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 142F12D43DCA4301A4FA8E25 /* INatIcon.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9E61060E5D7F4D7EADE6D77C /* INatIcon.ttf */; }; 191A91132CD1916800ECC774 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 191A91122CD1916800ECC774 /* InfoPlist.xcstrings */; }; 191A91142CD1916800ECC774 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 191A91122CD1916800ECC774 /* InfoPlist.xcstrings */; }; 191A91152CD1916800ECC774 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 191A91122CD1916800ECC774 /* InfoPlist.xcstrings */; }; @@ -35,6 +34,7 @@ AE4DC81B3A87484CB3FD6750 /* Lato-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4B0AEEF6CA584BCF9880EB35 /* Lato-Regular.ttf */; }; E23E0899594A7C6DF680FFDB /* libPods-iNaturalistReactNative-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A336AF0ADEAE537AB1B73F98 /* libPods-iNaturalistReactNative-ShareExtension.a */; }; E5DFC1C6FBFA45739CE91C69 /* Lato-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 69DF855D92EA4ADFB73B47F1 /* Lato-MediumItalic.ttf */; }; + 79C7ABE5C4BF4E65826F8414 /* INatIcon.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F3AB9FDE3E7D47CA9DB7D276 /* INatIcon.ttf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -102,12 +102,12 @@ 8C2D97D72EED451C887998A8 /* Lato-BoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Lato-BoldItalic.ttf"; path = "../assets/fonts/Lato-BoldItalic.ttf"; sourceTree = ""; }; 8F1AC6762BC1B610002F994B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 8F346E492CF6912700CED7B4 /* geomodel.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; path = geomodel.mlmodel; sourceTree = ""; }; - 9E61060E5D7F4D7EADE6D77C /* INatIcon.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = INatIcon.ttf; path = ../assets/fonts/INatIcon.ttf; sourceTree = ""; }; A336AF0ADEAE537AB1B73F98 /* libPods-iNaturalistReactNative-ShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iNaturalistReactNative-ShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; B8FC28F6DD66FAD52B79E072 /* Pods-iNaturalistReactNative.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative.debug.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative/Pods-iNaturalistReactNative.debug.xcconfig"; sourceTree = ""; }; D7AE5BDBC584A83878A04344 /* Pods-iNaturalistReactNative-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative-ShareExtension/Pods-iNaturalistReactNative-ShareExtension.debug.xcconfig"; sourceTree = ""; }; D8663889EABFBFC3077401E3 /* Pods-iNaturalistReactNative-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative-ShareExtension/Pods-iNaturalistReactNative-ShareExtension.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + F3AB9FDE3E7D47CA9DB7D276 /* INatIcon.ttf */ = {isa = PBXFileReference; name = "INatIcon.ttf"; path = "../assets/fonts/INatIcon.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -261,7 +261,7 @@ 3A9BAF07FCF24F668E2EF5AB /* Lato-Medium.ttf */, 69DF855D92EA4ADFB73B47F1 /* Lato-MediumItalic.ttf */, 4B0AEEF6CA584BCF9880EB35 /* Lato-Regular.ttf */, - 9E61060E5D7F4D7EADE6D77C /* INatIcon.ttf */, + F3AB9FDE3E7D47CA9DB7D276 /* INatIcon.ttf */, ); name = Resources; sourceTree = ""; @@ -398,7 +398,7 @@ 085DD3205807404CAFC32228 /* Lato-Medium.ttf in Resources */, E5DFC1C6FBFA45739CE91C69 /* Lato-MediumItalic.ttf in Resources */, AE4DC81B3A87484CB3FD6750 /* Lato-Regular.ttf in Resources */, - 142F12D43DCA4301A4FA8E25 /* INatIcon.ttf in Resources */, + 79C7ABE5C4BF4E65826F8414 /* INatIcon.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/link-assets-manifest.json b/ios/link-assets-manifest.json index 8b83c37e9..c38428621 100644 --- a/ios/link-assets-manifest.json +++ b/ios/link-assets-manifest.json @@ -3,7 +3,7 @@ "data": [ { "path": "assets/fonts/INatIcon.ttf", - "sha1": "4f576448e7cb11aa54c32307a73fcc265ea4a920" + "sha1": "dd93213119594cc223f9e40f6a69df60dd5578d0" }, { "path": "assets/fonts/Lato-Bold.ttf", diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js index d39fe9894..d28818114 100644 --- a/src/components/Camera/AICamera/AICamera.js +++ b/src/components/Camera/AICamera/AICamera.js @@ -36,6 +36,7 @@ import { import AICameraButtons from "./AICameraButtons"; import FrameProcessorCamera from "./FrameProcessorCamera"; import usePredictions from "./hooks/usePredictions"; +import LocationStatus from "./LocationStatus"; const isTablet = DeviceInfo.isTablet(); @@ -58,7 +59,9 @@ type Props = { takingPhoto: boolean, takePhotoAndStoreUri: Function, takePhotoOptions: Object, - userLocation?: Object // UserLocation | null + userLocation?: Object, // UserLocation | null + hasLocationPermissions: boolean, + requestLocationPermissions: () => void, }; const AICamera = ( { @@ -70,7 +73,9 @@ const AICamera = ( { takingPhoto, takePhotoAndStoreUri, takePhotoOptions, - userLocation + userLocation, + hasLocationPermissions, + requestLocationPermissions }: Props ): Node => { const navigation = useNavigation( ); const sentinelFileName = useStore( state => state.sentinelFileName ); @@ -108,6 +113,29 @@ const AICamera = ( { const [initialVolume, setInitialVolume] = useState( null ); const [hasTakenPhoto, setHasTakenPhoto] = useState( false ); + const [useLocation, setUseLocation] = useState( !!hasLocationPermissions ); + const [locationStatusVisible, setLocationStatusVisible] = useState( false ); + + const toggleLocation = () => { + if ( !useLocation && !hasLocationPermissions ) { + requestLocationPermissions( ); + return; + } + setUseLocation( prev => !prev ); + // Always show status when button is pressed + setLocationStatusVisible( true ); + }; + + const handleLocationStatusEnd = ( ) => { + setLocationStatusVisible( false ); + }; + + useEffect( ( ) => { + if ( hasLocationPermissions ) { + setUseLocation( true ); + } + }, [hasLocationPermissions] ); + const { t } = useTranslation(); const { loadTime } = usePerformance( { @@ -205,6 +233,7 @@ const AICamera = ( { inactive={inactive} resetCameraOnFocus={resetCameraOnFocus} userLocation={userLocation} + useLocation={useLocation} /> )} @@ -248,6 +277,11 @@ const AICamera = ( { : t( "Loading-iNaturalists-AI-Camera" )} )} + {isDebug && result && ( {`Age of result: ${Date.now() - result.timestamp}ms`} @@ -258,11 +292,7 @@ const AICamera = ( { {!modelLoaded && ( - + )} @@ -289,6 +319,8 @@ const AICamera = ( { takingPhoto={takingPhoto} toggleFlash={toggleFlash} zoomTextValue={zoomTextValue} + useLocation={useLocation} + toggleLocation={toggleLocation} /> ); diff --git a/src/components/Camera/AICamera/AICameraButtons.tsx b/src/components/Camera/AICamera/AICameraButtons.tsx index 2d8c33552..94f30ed2b 100644 --- a/src/components/Camera/AICamera/AICameraButtons.tsx +++ b/src/components/Camera/AICamera/AICameraButtons.tsx @@ -1,6 +1,7 @@ import CameraFlip from "components/Camera/Buttons/CameraFlip.tsx"; import Close from "components/Camera/Buttons/Close.tsx"; import Flash from "components/Camera/Buttons/Flash.tsx"; +import Location from "components/Camera/Buttons/Location.tsx"; import PhotoLibraryIcon from "components/Camera/Buttons/PhotoLibraryIcon.tsx"; import TakePhoto from "components/Camera/Buttons/TakePhoto.tsx"; import Zoom from "components/Camera/Buttons/Zoom.tsx"; @@ -10,6 +11,7 @@ import React from "react"; import { GestureResponderEvent, ViewStyle } from "react-native"; import DeviceInfo from "react-native-device-info"; import type { TakePhotoOptions } from "react-native-vision-camera"; +import { useLayoutPrefs } from "sharedHooks"; import AIDebugButton from "./AIDebugButton"; @@ -38,6 +40,8 @@ interface Props { takingPhoto: boolean; toggleFlash: ( _event: GestureResponderEvent ) => void; zoomTextValue: string; + useLocation: boolean; + toggleLocation: ( _event: GestureResponderEvent ) => void; } const AICameraButtons = ( { @@ -61,8 +65,11 @@ const AICameraButtons = ( { takePhotoOptions, takingPhoto, toggleFlash, - zoomTextValue + zoomTextValue, + useLocation, + toggleLocation }: Props ) => { + const { isDefaultMode } = useLayoutPrefs(); if ( isTablet ) { return ( ); } @@ -110,14 +120,24 @@ const AICameraButtons = ( { accessibilityRole="adjustable" accessibilityValue={{ min: 0, max: 100, now: 50 }} /> - - - + {!isDefaultMode && ( + + + + )} + {showZoomButton && ( + + + + )} { const sentinelFileName = useStore( state => state.sentinelFileName ); + const { isDefaultMode } = useLayoutPrefs( ); const { deviceOrientation } = useDeviceOrientation(); const [lastTimestamp, setLastTimestamp] = useState( undefined ); const navigation = useNavigation(); + // When useLocation changes, we need to reset the stored results + useEffect( () => { + InatVision.resetStoredResults(); + }, [useLocation] ); + useEffect( () => { // This registers a listener for the frame processor plugin's log events // iOS part exposes no logging, so calling it would crash @@ -132,6 +140,9 @@ const FrameProcessorCamera = ( { const patchedOrientationAndroid = orientationPatchFrameProcessor( deviceOrientation ); const patchedRunAsync = usePatchedRunAsync( ); const hasUserLocation = userLocation?.latitude != null && userLocation?.longitude != null; + const useGeomodel = isDefaultMode + ? hasUserLocation + : ( useLocation && hasUserLocation ); // The vision-plugin has a function to look up the location of the user in a h3 gridded world // unfortunately, I was not able to run this new function in the worklets directly, // so we need to do this here before calling the useFrameProcessor hook. @@ -170,7 +181,7 @@ const FrameProcessorCamera = ( { numStoredResults, cropRatio, patchedOrientationAndroid, - useGeomodel: hasUserLocation, + useGeomodel, geomodelPath, location: { latitude: geoModelCellLocation?.latitude, @@ -197,7 +208,7 @@ const FrameProcessorCamera = ( { cropRatio, lastTimestamp, fps, - hasUserLocation, + useGeomodel, geoModelCellLocation ] ); diff --git a/src/components/Camera/AICamera/LocationStatus.js b/src/components/Camera/AICamera/LocationStatus.js new file mode 100644 index 000000000..e056f6f8f --- /dev/null +++ b/src/components/Camera/AICamera/LocationStatus.js @@ -0,0 +1,51 @@ +import { Body1, INatIcon } from "components/SharedComponents"; +import { View } from "components/styledComponents"; +import React, { useEffect, useRef } from "react"; +import { Animated } from "react-native"; +import { useTranslation } from "sharedHooks"; +import colors from "styles/tailwindColors"; + +const LocationStatus = ( { useLocation, visible, onAnimationEnd } ) => { + const { t } = useTranslation(); + const opacity = useRef( new Animated.Value( 0 ) ).current; + + useEffect( () => { + if ( visible ) { + Animated.sequence( [ + Animated.timing( opacity, { + toValue: 1, + duration: 200, + useNativeDriver: true + } ), + Animated.delay( 2000 ), + Animated.timing( opacity, { + toValue: 0, + duration: 200, + useNativeDriver: true + } ) + ] ).start( () => onAnimationEnd() ); + } + }, [visible, opacity, onAnimationEnd] ); + + if ( !visible ) { + return null; + } + + const name = useLocation + ? "map-marker-outline" + : "map-marker-outline-off"; + const text = useLocation + ? t( "Using-location" ) + : t( "Ignoring-location" ); + + return ( + + + + {text} + + + ); +}; + +export default LocationStatus; diff --git a/src/components/Camera/Buttons/Location.tsx b/src/components/Camera/Buttons/Location.tsx new file mode 100644 index 000000000..70866ef80 --- /dev/null +++ b/src/components/Camera/Buttons/Location.tsx @@ -0,0 +1,53 @@ +// eslint-disable-next-line max-len +import TransparentCircleButton from "components/SharedComponents/Buttons/TransparentCircleButton.tsx"; +import React from "react"; +import { GestureResponderEvent, ViewStyle } from "react-native"; +import DeviceInfo from "react-native-device-info"; +import Animated from "react-native-reanimated"; +import { useTranslation } from "sharedHooks"; + +const isTablet = DeviceInfo.isTablet(); + +interface Props { + rotatableAnimatedStyle: ViewStyle; + toggleLocation: ( _event: GestureResponderEvent ) => void; + useLocation?: boolean; +} + +const Location = ( { + rotatableAnimatedStyle, + toggleLocation, + useLocation +}: Props ) => { + const { t } = useTranslation( ); + + let testID = ""; + let accessibilityHint = ""; + let name = ""; + if ( useLocation ) { + name = "map-marker-outline"; + testID = "location-button-label-location"; + accessibilityHint = t( "Disable-location" ); + } else { + name = "map-marker-outline-off"; + testID = "location-button-label-location-off"; + accessibilityHint = t( "Enable-location" ); + } + + return ( + + + + ); +}; + +export default Location; diff --git a/src/components/Camera/Buttons/Zoom.tsx b/src/components/Camera/Buttons/Zoom.tsx index f9382a23b..5e8451ca5 100644 --- a/src/components/Camera/Buttons/Zoom.tsx +++ b/src/components/Camera/Buttons/Zoom.tsx @@ -17,22 +17,16 @@ interface Props { handleZoomButtonPress: ( _event: GestureResponderEvent ) => void; zoomClassName?: string; zoomTextValue: string; - showZoomButton: boolean; } const Zoom = ( { rotatableAnimatedStyle, handleZoomButtonPress, zoomClassName, - zoomTextValue, - showZoomButton + zoomTextValue }: Props ) => { const { t } = useTranslation(); - if ( !showZoomButton ) { - return null; - } - return ( { // 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( ); + const { + hasPermissions: hasLocationPermissions, + renderPermissionsGate: renderLocationPermissionsGate, + requestPermissions: requestLocationPermissions + } = useLocationPermission( ); const { userLocation } = useWatchPosition( { - shouldFetchLocation: !!( hasPermissions ) + shouldFetchLocation: !!( hasLocationPermissions ) } ); const navigation = useNavigation( ); const { t } = useTranslation( ); @@ -100,13 +104,13 @@ const CameraContainer = ( ) => { const generateSentinelFile = async ( ) => { const fileName = await createSentinelFile( "AICamera" ); setSentinelFileName( fileName ); - if ( hasPermissions ) { + if ( hasLocationPermissions ) { await logStage( fileName, "fetch_user_location_start" ); } }; if ( cameraType !== "AI" ) { return; } generateSentinelFile( ); - }, [setSentinelFileName, cameraType, hasPermissions] ); + }, [setSentinelFileName, cameraType, hasLocationPermissions] ); const { hasPermissions: hasSavePhotoPermission, @@ -252,10 +256,10 @@ const CameraContainer = ( ) => { newPhotoUris={newPhotoUris} setNewPhotoUris={setNewPhotoUris} userLocation={userLocation} + hasLocationPermissions={hasLocationPermissions} + requestLocationPermissions={requestLocationPermissions} /> {showPhotoPermissionsGate && renderSavePhotoPermissionGate( { - // If the user does not give location permissions in any form, - // navigate to the location picker (if granted we just continue fetching the location) onRequestGranted: ( ) => console.log( "granted in save photo permission gate" ), onRequestBlocked: ( ) => console.log( "blocked in save photo permission gate" ), onModalHide: async ( ) => { @@ -266,6 +270,13 @@ const CameraContainer = ( ) => { } ); } } )} + {renderLocationPermissionsGate( { + onRequestGranted: ( ) => console.log( "granted in location permission gate" ), + onRequestBlocked: ( ) => console.log( "blocked in location permission gate" ), + onModalHide: async ( ) => { + await logStageIfAICamera( "request_location_permission_complete" ); + } + } )} ); }; diff --git a/src/components/Camera/CameraWithDevice.tsx b/src/components/Camera/CameraWithDevice.tsx index 2f014440f..f8d98f2be 100644 --- a/src/components/Camera/CameraWithDevice.tsx +++ b/src/components/Camera/CameraWithDevice.tsx @@ -22,7 +22,9 @@ interface Props { newPhotoUris: Array, setNewPhotoUris: Function, takePhotoOptions: Object, - userLocation: UserLocation | null + userLocation: UserLocation | null, + hasLocationPermissions: boolean, + requestLocationPermissions: () => void, } const CameraWithDevice = ( { @@ -37,7 +39,9 @@ const CameraWithDevice = ( { newPhotoUris, setNewPhotoUris, takePhotoOptions, - userLocation + userLocation, + hasLocationPermissions, + requestLocationPermissions }: Props ) => { const { isLandscapeMode } = useDeviceOrientation( ); const flexDirection = isTablet && isLandscapeMode @@ -76,6 +80,8 @@ const CameraWithDevice = ( { takePhotoAndStoreUri={takePhotoAndStoreUri} takePhotoOptions={takePhotoOptions} userLocation={userLocation} + hasLocationPermissions={hasLocationPermissions} + requestLocationPermissions={requestLocationPermissions} /> )} diff --git a/src/components/Camera/TabletButtons.tsx b/src/components/Camera/TabletButtons.tsx index 16f025d7b..021155495 100644 --- a/src/components/Camera/TabletButtons.tsx +++ b/src/components/Camera/TabletButtons.tsx @@ -11,6 +11,7 @@ import type { TakePhotoOptions } from "react-native-vision-camera"; import CameraFlip from "./Buttons/CameraFlip"; import Flash from "./Buttons/Flash"; import GreenCheckmark from "./Buttons/GreenCheckmark"; +import Location from "./Buttons/Location"; import TakePhoto from "./Buttons/TakePhoto"; import Zoom from "./Buttons/Zoom"; @@ -52,6 +53,9 @@ interface Props { takePhotoOptions: TakePhotoOptions; toggleFlash: ( _event: GestureResponderEvent ) => void; zoomTextValue: string; + useLocation: boolean; + toggleLocation: ( _event: GestureResponderEvent ) => void; + isDefaultMode: boolean; } // Empty space where a camera button should be so buttons don't jump around @@ -84,7 +88,10 @@ const TabletButtons = ( { takePhoto, takePhotoOptions, toggleFlash, - zoomTextValue + zoomTextValue, + useLocation, + toggleLocation, + isDefaultMode }: Props ) => { const tabletCameraOptionsClasses = [ "absolute", @@ -99,6 +106,13 @@ const TabletButtons = ( { return ( { photosTaken && } + {!isDefaultMode && ( + + )} - + {t( "You-are-offline-Tap-to-reload" )} - { t( "Offline-suggestions-do-not-use-your-location" ) } + { t( "Offline-suggestions-may-differ-from-online" ) } ); diff --git a/src/i18n/l10n/en.ftl b/src/i18n/l10n/en.ftl index ff5a2b42b..e562f57cf 100644 --- a/src/i18n/l10n/en.ftl +++ b/src/i18n/l10n/en.ftl @@ -407,6 +407,8 @@ Device-storage-full = Device storage full Device-storage-full-description = iNaturalist may not be able to save your photos or may crash. # Button that disables the camera's flash Disable-flash = Disable flash +# Button that disables the camera to use location for suggestions +Disable-location = Disable location # Disagreement notice with an identificaiton, <0/> will get replaced by a # taxon name Disagreement = *@{ $username } disagrees this is <0/> @@ -455,6 +457,8 @@ EMAIL = EMAIL EMAIL-DEBUG-LOGS = EMAIL DEBUG LOGS # Button that enables the camera's flash Enable-flash = Enable flash +# Button that enables the camera to use location for suggestions +Enable-location = Enable location # Button that subscribes the user to notifications for an observation Enable-notifications = Enable notifications # Indicates a species only occurs in a specific place @@ -516,7 +520,7 @@ Flag-Item-Other-Description = Some other reason you can explain below. Flag-Item-Other-Input-Hint = Specify the reason you're flagging this item # Status when an item has been flagged Flagged = Flagged -Flash = flash +Flash = Flash # Label for a button that toggles between the front and back cameras Flip-camera = Flip camera FOLLOW = FOLLOW @@ -587,6 +591,7 @@ If-youre-seeing-this-error = If you're seeing this and you're online, iNat staff IGNORE-LOCATION = IGNORE LOCATION # Button to stop recieving notifications about observation Ignore-notifications = Ignore notifications +Ignoring-location = Ignoring location Import-Photos-From = Import Photos From # Shows the number of observations a user is about to import IMPORT-X-OBSERVATIONS = @@ -876,7 +881,7 @@ October = October Offensive-Inappropriate = Offensive/Inappropriate Offensive-Inappropriate-Examples = Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of "appropriate," see the FAQ. Offline-DQA-description = The DQA may not be accurate. Check your internet connection and try again. -Offline-suggestions-do-not-use-your-location = Offline suggestions do not use your location and may differ from online suggestions. Taxon images and common names may not load. +Offline-suggestions-may-differ-from-online = Offline suggestions may differ from online suggestions, and taxon images and common names may not load. # Generic confirmation, e.g. button on a warning alert OK = OK # Sort order, refers to newest or oldest date @@ -1318,6 +1323,7 @@ User = User { $userHandle } USERNAME-OR-EMAIL = USERNAME OR EMAIL # label in project requirements Users = Users +Using-location = Using location # Listing of app and build versions Version-app-build = Version { $appVersion } ({ $buildVersion }) VIEW-ALL-X-PLACES = VIEW ALL { $count } PLACES diff --git a/src/i18n/l10n/en.ftl.json b/src/i18n/l10n/en.ftl.json index 6fd0b5cd4..b4eb0fd75 100644 --- a/src/i18n/l10n/en.ftl.json +++ b/src/i18n/l10n/en.ftl.json @@ -214,6 +214,7 @@ "Device-storage-full": "Device storage full", "Device-storage-full-description": "iNaturalist may not be able to save your photos or may crash.", "Disable-flash": "Disable flash", + "Disable-location": "Disable location", "Disagreement": "*@{ $username } disagrees this is <0/>", "DISCARD": "DISCARD", "DISCARD-ALL": "DISCARD ALL", @@ -244,6 +245,7 @@ "EMAIL": "EMAIL", "EMAIL-DEBUG-LOGS": "EMAIL DEBUG LOGS", "Enable-flash": "Enable flash", + "Enable-location": "Enable location", "Enable-notifications": "Enable notifications", "Endemic": "Endemic", "Endemic-to-place": "Endemic to { $place }", @@ -291,7 +293,7 @@ "Flag-Item-Other-Description": "Some other reason you can explain below.", "Flag-Item-Other-Input-Hint": "Specify the reason you're flagging this item", "Flagged": "Flagged", - "Flash": "flash", + "Flash": "Flash", "Flip-camera": "Flip camera", "FOLLOW": "FOLLOW", "FOLLOWING--notifications": "FOLLOWING", @@ -332,6 +334,7 @@ "If-youre-seeing-this-error": "If you're seeing this and you're online, iNat staff have already been notified. Thanks for finding a bug! If you're offline, please take a screenshot and send us an email when you're back on the Internet.", "IGNORE-LOCATION": "IGNORE LOCATION", "Ignore-notifications": "Ignore notifications", + "Ignoring-location": "Ignoring location", "Import-Photos-From": "Import Photos From", "IMPORT-X-OBSERVATIONS": "IMPORT { $count ->\n [one] 1 OBSERVATION\n *[other] { $count } OBSERVATIONS\n}", "IMPROVE-THESE-SUGGESTIONS-BY-USING-YOUR-LOCATION": "IMPROVE THESE SUGGESTIONS BY USING YOUR LOCATION", @@ -518,7 +521,7 @@ "Offensive-Inappropriate": "Offensive/Inappropriate", "Offensive-Inappropriate-Examples": "Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of \"appropriate,\" see the FAQ.", "Offline-DQA-description": "The DQA may not be accurate. Check your internet connection and try again.", - "Offline-suggestions-do-not-use-your-location": "Offline suggestions do not use your location and may differ from online suggestions. Taxon images and common names may not load.", + "Offline-suggestions-may-differ-from-online": "Offline suggestions may differ from online suggestions, and taxon images and common names may not load.", "OK": "OK", "Oldest-to-newest": "Oldest to newest", "Once-you-create-and-upload-observations": "Once you create & upload observations, other members of our community can add identifications to help your observations reach research grade.", @@ -847,6 +850,7 @@ "User": "User { $userHandle }", "USERNAME-OR-EMAIL": "USERNAME OR EMAIL", "Users": "Users", + "Using-location": "Using location", "Version-app-build": "Version { $appVersion } ({ $buildVersion })", "VIEW-ALL-X-PLACES": "VIEW ALL { $count } PLACES", "VIEW-ALL-X-PROJECTS": "VIEW ALL { $count } PROJECTS", diff --git a/src/i18n/strings.ftl b/src/i18n/strings.ftl index ff5a2b42b..e562f57cf 100644 --- a/src/i18n/strings.ftl +++ b/src/i18n/strings.ftl @@ -407,6 +407,8 @@ Device-storage-full = Device storage full Device-storage-full-description = iNaturalist may not be able to save your photos or may crash. # Button that disables the camera's flash Disable-flash = Disable flash +# Button that disables the camera to use location for suggestions +Disable-location = Disable location # Disagreement notice with an identificaiton, <0/> will get replaced by a # taxon name Disagreement = *@{ $username } disagrees this is <0/> @@ -455,6 +457,8 @@ EMAIL = EMAIL EMAIL-DEBUG-LOGS = EMAIL DEBUG LOGS # Button that enables the camera's flash Enable-flash = Enable flash +# Button that enables the camera to use location for suggestions +Enable-location = Enable location # Button that subscribes the user to notifications for an observation Enable-notifications = Enable notifications # Indicates a species only occurs in a specific place @@ -516,7 +520,7 @@ Flag-Item-Other-Description = Some other reason you can explain below. Flag-Item-Other-Input-Hint = Specify the reason you're flagging this item # Status when an item has been flagged Flagged = Flagged -Flash = flash +Flash = Flash # Label for a button that toggles between the front and back cameras Flip-camera = Flip camera FOLLOW = FOLLOW @@ -587,6 +591,7 @@ If-youre-seeing-this-error = If you're seeing this and you're online, iNat staff IGNORE-LOCATION = IGNORE LOCATION # Button to stop recieving notifications about observation Ignore-notifications = Ignore notifications +Ignoring-location = Ignoring location Import-Photos-From = Import Photos From # Shows the number of observations a user is about to import IMPORT-X-OBSERVATIONS = @@ -876,7 +881,7 @@ October = October Offensive-Inappropriate = Offensive/Inappropriate Offensive-Inappropriate-Examples = Misleading or illegal content, racial or ethnic slurs, etc. For more on our definition of "appropriate," see the FAQ. Offline-DQA-description = The DQA may not be accurate. Check your internet connection and try again. -Offline-suggestions-do-not-use-your-location = Offline suggestions do not use your location and may differ from online suggestions. Taxon images and common names may not load. +Offline-suggestions-may-differ-from-online = Offline suggestions may differ from online suggestions, and taxon images and common names may not load. # Generic confirmation, e.g. button on a warning alert OK = OK # Sort order, refers to newest or oldest date @@ -1318,6 +1323,7 @@ User = User { $userHandle } USERNAME-OR-EMAIL = USERNAME OR EMAIL # label in project requirements Users = Users +Using-location = Using location # Listing of app and build versions Version-app-build = Version { $appVersion } ({ $buildVersion }) VIEW-ALL-X-PLACES = VIEW ALL { $count } PLACES diff --git a/src/images/icons/map-marker-outline-off.svg b/src/images/icons/map-marker-outline-off.svg new file mode 100644 index 000000000..7d550b210 --- /dev/null +++ b/src/images/icons/map-marker-outline-off.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/unit/components/BottomTabNavigator/__snapshots__/CustomTabBar.test.js.snap b/tests/unit/components/BottomTabNavigator/__snapshots__/CustomTabBar.test.js.snap index 11d5c90ad..b47f7aad6 100644 --- a/tests/unit/components/BottomTabNavigator/__snapshots__/CustomTabBar.test.js.snap +++ b/tests/unit/components/BottomTabNavigator/__snapshots__/CustomTabBar.test.js.snap @@ -163,7 +163,7 @@ exports[`CustomTabBar with advanced user layout should render correctly 1`] = ` ] } > -  +  @@ -307,7 +307,7 @@ exports[`CustomTabBar with advanced user layout should render correctly 1`] = ` ] } > -  +  diff --git a/tests/unit/components/SharedComponents/ActivityCount/__snapshots__/ActivityCount.test.js.snap b/tests/unit/components/SharedComponents/ActivityCount/__snapshots__/ActivityCount.test.js.snap index 1fd5608d0..089eef5e8 100644 --- a/tests/unit/components/SharedComponents/ActivityCount/__snapshots__/ActivityCount.test.js.snap +++ b/tests/unit/components/SharedComponents/ActivityCount/__snapshots__/ActivityCount.test.js.snap @@ -36,7 +36,7 @@ exports[`ActivityCount renders reliably 1`] = ` ] } > -  +  -  +  -  +  -  +  -  +  -  +  -  +  -  +  diff --git a/tests/unit/components/SharedComponents/ObservationsFlashList/__snapshots__/ObsGridItem.test.js.snap b/tests/unit/components/SharedComponents/ObservationsFlashList/__snapshots__/ObsGridItem.test.js.snap index 8815da793..72afab54d 100644 --- a/tests/unit/components/SharedComponents/ObservationsFlashList/__snapshots__/ObsGridItem.test.js.snap +++ b/tests/unit/components/SharedComponents/ObservationsFlashList/__snapshots__/ObsGridItem.test.js.snap @@ -117,7 +117,7 @@ exports[`ObsGridItem for an observation with a photo should render 1`] = ` ] } > -  +  @@ -359,7 +359,7 @@ exports[`ObsGridItem for an observation without a photo should render 1`] = ` ] } > -  +  diff --git a/tests/unit/components/SharedComponents/UploadStatus/__snapshots__/UploadStatus.test.js.snap b/tests/unit/components/SharedComponents/UploadStatus/__snapshots__/UploadStatus.test.js.snap index f5b0aa57d..25eeae4ac 100644 --- a/tests/unit/components/SharedComponents/UploadStatus/__snapshots__/UploadStatus.test.js.snap +++ b/tests/unit/components/SharedComponents/UploadStatus/__snapshots__/UploadStatus.test.js.snap @@ -114,7 +114,7 @@ exports[`UploadStatus displays complete icon when progress is 1 1`] = ` ] } > -  +  @@ -238,7 +238,7 @@ exports[`UploadStatus displays progress bar when progress is greater than 5% 1`] ] } > -  +  -  +  -  +  @@ -524,7 +524,7 @@ exports[`UploadStatus displays start icon when upload is unsynced and not queued ] } > -  +  -  +  diff --git a/tests/unit/components/SharedComponents/__snapshots__/Checkbox.test.js.snap b/tests/unit/components/SharedComponents/__snapshots__/Checkbox.test.js.snap index 0d008f650..3de06b0ff 100644 --- a/tests/unit/components/SharedComponents/__snapshots__/Checkbox.test.js.snap +++ b/tests/unit/components/SharedComponents/__snapshots__/Checkbox.test.js.snap @@ -278,7 +278,7 @@ exports[`Checkbox renders reliably being checked 1`] = ` ] } > -  +  diff --git a/tests/unit/components/SharedComponents/__snapshots__/TaxonResult.test.js.snap b/tests/unit/components/SharedComponents/__snapshots__/TaxonResult.test.js.snap index f9930259e..633f59fb5 100644 --- a/tests/unit/components/SharedComponents/__snapshots__/TaxonResult.test.js.snap +++ b/tests/unit/components/SharedComponents/__snapshots__/TaxonResult.test.js.snap @@ -255,7 +255,7 @@ exports[`TaxonResult should render correctly 1`] = ` ] } > -  +  @@ -511,7 +511,7 @@ exports[`TaxonResult should render correctly 1`] = ` ] } > -  +  @@ -606,7 +606,7 @@ exports[`TaxonResult should render correctly 1`] = ` ] } > -  +  diff --git a/tests/unit/components/__snapshots__/INatIcon.test.js.snap b/tests/unit/components/__snapshots__/INatIcon.test.js.snap index 13b66324c..0e81b16a9 100644 --- a/tests/unit/components/__snapshots__/INatIcon.test.js.snap +++ b/tests/unit/components/__snapshots__/INatIcon.test.js.snap @@ -20,6 +20,6 @@ exports[`INatIcon renders correctly 1`] = ` ] } > -  +  `;