Merge branch 'main' into mob-512-implement-saved-match-screen

This commit is contained in:
sepeterson
2025-11-25 08:11:28 -06:00
45 changed files with 771 additions and 798 deletions

View File

@@ -106,8 +106,8 @@ android {
applicationId "org.inaturalist.iNaturalistMobile"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 185
versionName "1.0.11"
versionCode 186
versionName "1.0.12"
setProperty("archivesBaseName", applicationId + "-v" + versionName + "+" + versionCode)
manifestPlaceholders = [ GMAPS_API_KEY:project.env.get("GMAPS_API_KEY") ]
// Detox Android setup

View File

@@ -23,7 +23,7 @@ describe( "Signed in user", () => {
const addObsButton = element( by.id( "add-obs-button" ) );
await waitFor( addObsButton ).toBeVisible().withTimeout( TIMEOUT );
await addObsButton.tap();
await expect( element( by.id( "identify-text" ) ) ).toBeVisible();
await expect( element( by.id( "observe-without-evidence-button" ) ) ).toBeVisible();
// Observe without evidence
const obsWithoutEvidenceButton = element(
by.id( "observe-without-evidence-button" )

View File

@@ -0,0 +1,2 @@
UPDATED
* Minor dependency updates

View File

@@ -2042,7 +2042,37 @@ PODS:
- Yoga
- react-native-sensitive-info (6.0.0-alpha.9):
- React-Core
- react-native-slider (4.5.0):
- react-native-slider (5.1.1):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- react-native-slider/common (= 5.1.1)
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-slider/common (5.1.1):
- boost
- DoubleConversion
- fast_float
@@ -3099,7 +3129,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNSVG (15.12.0):
- RNSVG (15.15.0):
- boost
- DoubleConversion
- fast_float
@@ -3126,10 +3156,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.12.0)
- RNSVG/common (= 15.15.0)
- SocketRocket
- Yoga
- RNSVG/common (15.12.0):
- RNSVG/common (15.15.0):
- boost
- DoubleConversion
- fast_float
@@ -3659,7 +3689,7 @@ SPEC CHECKSUMS:
react-native-restart: 0bc732f4461709022a742bb29bcccf6bbc5b4863
react-native-safe-area-context: 849813645d0fd16fd899a02ed6e03320284ec2ae
react-native-sensitive-info: ee358bf2b901ac3d04f63ff637b31daee44ea87f
react-native-slider: fa04ab42a8333ff6bcf2c77ae76885d7f7c3d645
react-native-slider: 23f01fbf6c2413572dc26fbb80dd66fe573c7692
react-native-volume-manager: cdd3c3857158c1df7b9fbea071a9946395cee06c
react-native-webview: 44434e525f798a7b35b8b94d9db36cd25a75048f
react-native-worklets-core: be5da7693070046d10b05ec6143f2f19bf05aa98
@@ -3713,7 +3743,7 @@ SPEC CHECKSUMS:
RNScreens: eff474e98669c2a4dbe7e43ee9464682711182ea
RNShareMenu: ca0a55c45650bc7dffb47bf81a0bd6cd299a330e
RNStoreReview: 77a0cc49341ad0e36e1860d42c70b05b2dfe5086
RNSVG: bc7ccfe884848ac924d2279d9025d41b5f05cb0c
RNSVG: 9965245a32db769d6f959dbc783cb5037d77e690
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
VisionCamera: 4146fa2612c154f893a42a9b1feedf868faa6b23
VisionCameraPluginInatVision: 212f5cfd74939272b73d5f73a071dd0ea42beed3

View File

@@ -548,7 +548,7 @@
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNative.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CURRENT_PROJECT_VERSION = 185;
CURRENT_PROJECT_VERSION = 186;
DEVELOPMENT_TEAM = N5J7L4P93Z;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -675,7 +675,7 @@
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNativeRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CURRENT_PROJECT_VERSION = 185;
CURRENT_PROJECT_VERSION = 186;
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
HEADER_SEARCH_PATHS = (
@@ -988,7 +988,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "iNaturalistReactNative-ShareExtension/iNaturalistReactNative-ShareExtension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 185;
CURRENT_PROJECT_VERSION = 186;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
@@ -1033,7 +1033,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 185;
CURRENT_PROJECT_VERSION = 186;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = N5J7L4P93Z;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.11</string>
<string>1.0.12</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -40,7 +40,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>185</string>
<string>186</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>

142
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "inaturalistreactnative",
"version": "1.0.11",
"version": "1.0.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "inaturalistreactnative",
"version": "1.0.11",
"version": "1.0.12",
"hasInstallScript": true,
"dependencies": {
"@bam.tech/react-native-image-resizer": "^3.0.11",
@@ -21,7 +21,7 @@
"@react-native-community/geolocation": "^3.4.0",
"@react-native-community/hooks": "^3.0.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-community/slider": "^4.5.0",
"@react-native-community/slider": "^5.1.1",
"@react-native-firebase/analytics": "^23.4.0",
"@react-native-firebase/app": "^23.3.1",
"@react-native-google-signin/google-signin": "^13.1.0",
@@ -93,7 +93,7 @@
"react-native-sensitive-info": "^6.0.0-alpha.9",
"react-native-share-menu": "github:inaturalist/react-native-share-menu#iNaturalistReactNative",
"react-native-store-review": "^0.4.3",
"react-native-svg": "^15.12.0",
"react-native-svg": "^15.15.0",
"react-native-svg-transformer": "^1.3.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-vision-camera": "^4.7.2",
@@ -114,9 +114,9 @@
"@babel/runtime": "^7.25.0",
"@faker-js/faker": "^8.4.1",
"@fluent/syntax": "^0.19.0",
"@react-native-community/cli": "19.1.1",
"@react-native-community/cli-platform-android": "19.1.1",
"@react-native-community/cli-platform-ios": "19.1.1",
"@react-native-community/cli": "19.1.2",
"@react-native-community/cli-platform-android": "19.1.2",
"@react-native-community/cli-platform-ios": "19.1.2",
"@react-native/babel-preset": "0.80.2",
"@react-native/eslint-config": "0.80.2",
"@react-native/metro-config": "0.80.2",
@@ -4434,18 +4434,18 @@
}
},
"node_modules/@react-native-community/cli": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-19.1.1.tgz",
"integrity": "sha512-H17sV83KPg2H2GCNuUSMM1ZM2sy6msVSmxrhJSycH8ua3i9Iixja8DeYtGIcJUzjdU/4U2eSDs6PjOSZUVn8CQ==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-19.1.2.tgz",
"integrity": "sha512-b28TLqODMgQRx6f4gbHoHYpnKyFbWzJkIk3+Ggpad/at493KfGQ+WvKg1sts/st8mxzmbk0T6lCc/9A3QoFKkQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-clean": "19.1.1",
"@react-native-community/cli-config": "19.1.1",
"@react-native-community/cli-doctor": "19.1.1",
"@react-native-community/cli-server-api": "19.1.1",
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-types": "19.1.1",
"@react-native-community/cli-clean": "19.1.2",
"@react-native-community/cli-config": "19.1.2",
"@react-native-community/cli-doctor": "19.1.2",
"@react-native-community/cli-server-api": "19.1.2",
"@react-native-community/cli-tools": "19.1.2",
"@react-native-community/cli-types": "19.1.2",
"chalk": "^4.1.2",
"commander": "^9.4.1",
"deepmerge": "^4.3.0",
@@ -4464,13 +4464,13 @@
}
},
"node_modules/@react-native-community/cli-clean": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-19.1.1.tgz",
"integrity": "sha512-pP7SmK+PNw5B1Aa2c6y06FBNc9iGah/leFFM2uewpyZRJQ4zycX6Zz1UANpq9YZfp65n7NZKV9Gct2uaVRuP/Q==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-19.1.2.tgz",
"integrity": "sha512-LI/bTLtosbDyHtIs+HxlmHp+5Nbjz+IIEEqrBO2tUeA+ENX01YEnIgGIv4z7giNWkHSiqywjdOyYNqg27ydy2g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-tools": "19.1.2",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-glob": "^3.3.2"
@@ -4530,13 +4530,13 @@
"license": "MIT"
},
"node_modules/@react-native-community/cli-config": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-19.1.1.tgz",
"integrity": "sha512-qGLYCFf3whCa/we3iKd5BY4RlcAUhSykwGpnJpjseXLaI5iJzIn/IMd70EBG8QvhV/KQxM7VFMQj6KgGcoNKYg==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-19.1.2.tgz",
"integrity": "sha512-o0cc6R6r9nY9MiLFeLIN797fBLWwKW9cee/NCm6nBBzPk/paro6HEbcXE02xnVzMb+nhQPrbPOzp3qE7WhtwRA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-tools": "19.1.2",
"chalk": "^4.1.2",
"cosmiconfig": "^9.0.0",
"deepmerge": "^4.3.0",
@@ -4545,13 +4545,13 @@
}
},
"node_modules/@react-native-community/cli-config-android": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config-android/-/cli-config-android-19.1.1.tgz",
"integrity": "sha512-uAUXU/BPuasBy7For5lvVEpxiwA29X5BWKjM4fgxWmsQhaZHW//6PNRep94w3WVnAp+CUbW6+o3SzFqMX0PdIw==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config-android/-/cli-config-android-19.1.2.tgz",
"integrity": "sha512-IIhzhDUmT53RT45Qrxc/OfvkTD4U7IrfkfoIdKmBT6O0X0QaoegK4OE6aAuc86D2GXlD5rbVcPMSuN4TY8Hmlw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-tools": "19.1.2",
"chalk": "^4.1.2",
"fast-glob": "^3.3.2",
"fast-xml-parser": "^4.4.1"
@@ -4611,13 +4611,13 @@
"license": "MIT"
},
"node_modules/@react-native-community/cli-config-apple": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-19.1.1.tgz",
"integrity": "sha512-dKS7pg5eAEgRB8sOWYpr6XCR/3xUcttHNsuYYbuMXfY9d0M3d0oGquuMOW/p3Ri9sJI16bRAs/YIXDF2m4gYIA==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-19.1.2.tgz",
"integrity": "sha512-91upuYMLgEtJE6foWQFgGDpT3ZDTc5bX6rMY5cJMqiAE5svgh1q0kbbpRuv/ptBYzcxLplL7wZWpA77TlJdm9A==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-tools": "19.1.2",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-glob": "^3.3.2"
@@ -4730,17 +4730,17 @@
"license": "MIT"
},
"node_modules/@react-native-community/cli-doctor": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-19.1.1.tgz",
"integrity": "sha512-P6JgTpa8fn6SfGiotyRhiCqBlRlKx8MUUdMESPGyPzvMb8omz+Jv0ibdNg9CVT11/0x5oRsoGv07os/o+Eg0zQ==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-19.1.2.tgz",
"integrity": "sha512-uUV/1QrWA1Cx7dqkTCcarqfya/7gBmKXd9BzVCEl6bzAn1jd1Q5UaZ+DmZgAoLVKlbAjpPTJTfqjD44aqUdjyA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config": "19.1.1",
"@react-native-community/cli-platform-android": "19.1.1",
"@react-native-community/cli-platform-apple": "19.1.1",
"@react-native-community/cli-platform-ios": "19.1.1",
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-config": "19.1.2",
"@react-native-community/cli-platform-android": "19.1.2",
"@react-native-community/cli-platform-apple": "19.1.2",
"@react-native-community/cli-platform-ios": "19.1.2",
"@react-native-community/cli-tools": "19.1.2",
"chalk": "^4.1.2",
"command-exists": "^1.2.8",
"deepmerge": "^4.3.0",
@@ -4820,14 +4820,14 @@
}
},
"node_modules/@react-native-community/cli-platform-android": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-19.1.1.tgz",
"integrity": "sha512-omEAcIYz22Lxi/WjYHkNaUMEKV+o60PL3DJE6Wz3c4bkuDfxICJ8JcPawT4fDMsBX7DYwnYf6/Lk/leqQmHzOw==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-19.1.2.tgz",
"integrity": "sha512-eMryTlSSTl3JK/tZTaMaMgHec9qu+eQj+3A15qmBdj2ac3p/hiauwAe4q35rz5XABw1cJCuyn+s469YsdTllaw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config-android": "19.1.1",
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-config-android": "19.1.2",
"@react-native-community/cli-tools": "19.1.2",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"logkitty": "^0.7.1"
@@ -4887,14 +4887,14 @@
"license": "MIT"
},
"node_modules/@react-native-community/cli-platform-apple": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-19.1.1.tgz",
"integrity": "sha512-nsJ/TlQ97Lcmz5dVZVSwYYQzJmK6q/9X31VTAFhUf94ShugF3zXjaNnOJieKYDJlXy4G0EnrEulX1gTt29ebyw==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-19.1.2.tgz",
"integrity": "sha512-TtaF8Pyrs4dnIH3LTvuPnPjGDsSVaZLu+8s4y5bngzZIf9r7M/HJTlpnhm8+bQPsahxNhNQZBGUBrQJqfmg7Ww==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config-apple": "19.1.1",
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-config-apple": "19.1.2",
"@react-native-community/cli-tools": "19.1.2",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-xml-parser": "^4.4.1"
@@ -4954,23 +4954,23 @@
"license": "MIT"
},
"node_modules/@react-native-community/cli-platform-ios": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-19.1.1.tgz",
"integrity": "sha512-QHw/eBszq+62xUBorVqjgDYsVrZ5JAYJZkc6UKO327LnVn10OUB/bPGA/FzDWZdGB77pt0IalNP8nxyGOytMfg==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-19.1.2.tgz",
"integrity": "sha512-rmLZjwpI+mV3bbd6FgR6yM/ekFNr4QM/Dgzmatkh8k94B5uGtw5Me4EKlY+MrqR3lIyjzqWtLoefcJxA1c9d2w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-platform-apple": "19.1.1"
"@react-native-community/cli-platform-apple": "19.1.2"
}
},
"node_modules/@react-native-community/cli-server-api": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-19.1.1.tgz",
"integrity": "sha512-p0FFm82uPrtLZBWTD3bZ43mMBIV5mXwvGFYMcsfGiuMoS9SNbw4ImEFTG2IutVpr7Qb6NMjx6SbgYYMnTdZXmw==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-19.1.2.tgz",
"integrity": "sha512-K6UIvtw6VtcKxCX+rJ5mKQYiqcSSRKODPQ2nbIeIxjjO5nDjDriGkFC/ypHHk38oZuJYOLbOySqnnCNkdEI4uQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "19.1.1",
"@react-native-community/cli-tools": "19.1.2",
"body-parser": "^1.20.3",
"compression": "^1.7.1",
"connect": "^3.6.5",
@@ -5096,9 +5096,9 @@
}
},
"node_modules/@react-native-community/cli-tools": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-19.1.1.tgz",
"integrity": "sha512-0yWOdrfgO7jVtYzhNcm9hTA1hqrD6haqDaesFq4d3YCmh8lkkTb61Q/kNIKQCUfaCTR/Qcc4mdwy6ObdXRoTIQ==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-19.1.2.tgz",
"integrity": "sha512-AsDuZu/7R/QX+vGpJIRK97v24X+zqkmwA9/uLRguLTHM175nUxb/byXmAKWuZylG2FAikVvf7EqV8MFGbwM7Wg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5256,9 +5256,9 @@
}
},
"node_modules/@react-native-community/cli-types": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-19.1.1.tgz",
"integrity": "sha512-rOGiYjeDM9tkYBEuK6TJrnxpMhmaId1Un8pjQJswz7W9w2Vb6+nnLfWja7X7VmDIvqIK5GhVobRHsmKCKIdDEA==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-19.1.2.tgz",
"integrity": "sha512-Ze6fi6jE+JPvMlISWbZ/eCPOkRuuEs1SX4rJGWOXPcDzEVF6gs1ePsAjdzQ3RJYRMqQ49vo6iGiOZs//z5kuVw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5459,9 +5459,10 @@
}
},
"node_modules/@react-native-community/slider": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.0.tgz",
"integrity": "sha512-pyUvNTvu5IfCI5abzqRfO/dd3A009RC66RXZE6t0gyOwI/j0QDlq9VZRv3rjkpuIvNTnsYj+m5BHlh0DkSYUyA=="
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-5.1.1.tgz",
"integrity": "sha512-W98If/LnTaziU3/0h5+G1LvJaRhMc6iLQBte6UWa4WBIHDMaDPglNBIFKcCXc9Dxp83W+f+5Wv22Olq9M2HJYA==",
"license": "MIT"
},
"node_modules/@react-native-firebase/analytics": {
"version": "23.4.0",
@@ -20309,9 +20310,10 @@
"integrity": "sha512-RSQ6vx2j4p41GwTqNv2VV7yold62j5qDbGEBAjFi6gkXMrMpxFMg+82FPjbh6012tqv6Ebzwfqw6S4m4d7sddw=="
},
"node_modules/react-native-svg": {
"version": "15.12.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.0.tgz",
"integrity": "sha512-iE25PxIJ6V0C6krReLquVw6R0QTsRTmEQc4K2Co3P6zsimU/jltcDBKYDy1h/5j9S/fqmMeXnpM+9LEWKJKI6A==",
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.0.tgz",
"integrity": "sha512-/Wx6F/IZ88B/GcF88bK8K7ZseJDYt+7WGaiggyzLvTowChQ8BM5idmcd4pK+6QJP6a6DmzL2sfOMukFUn/NArg==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",

View File

@@ -1,6 +1,6 @@
{
"name": "inaturalistreactnative",
"version": "1.0.11",
"version": "1.0.12",
"private": true,
"scripts": {
"android": "react-native run-android",
@@ -57,7 +57,7 @@
"@react-native-community/geolocation": "^3.4.0",
"@react-native-community/hooks": "^3.0.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-community/slider": "^4.5.0",
"@react-native-community/slider": "^5.1.1",
"@react-native-firebase/analytics": "^23.4.0",
"@react-native-firebase/app": "^23.3.1",
"@react-native-google-signin/google-signin": "^13.1.0",
@@ -129,7 +129,7 @@
"react-native-sensitive-info": "^6.0.0-alpha.9",
"react-native-share-menu": "github:inaturalist/react-native-share-menu#iNaturalistReactNative",
"react-native-store-review": "^0.4.3",
"react-native-svg": "^15.12.0",
"react-native-svg": "^15.15.0",
"react-native-svg-transformer": "^1.3.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-vision-camera": "^4.7.2",
@@ -150,9 +150,9 @@
"@babel/runtime": "^7.25.0",
"@faker-js/faker": "^8.4.1",
"@fluent/syntax": "^0.19.0",
"@react-native-community/cli": "19.1.1",
"@react-native-community/cli-platform-android": "19.1.1",
"@react-native-community/cli-platform-ios": "19.1.1",
"@react-native-community/cli": "19.1.2",
"@react-native-community/cli-platform-android": "19.1.2",
"@react-native-community/cli-platform-ios": "19.1.2",
"@react-native/babel-preset": "0.80.2",
"@react-native/eslint-config": "0.80.2",
"@react-native/metro-config": "0.80.2",

View File

@@ -0,0 +1,169 @@
import {
Body3, BottomSheet, INatIcon, INatIconButton
} from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import React, { useMemo } from "react";
import { Platform } from "react-native";
import Observation from "realmModels/Observation";
import { useTranslation } from "sharedHooks";
import useStore from "stores/useStore";
import colors from "styles/tailwindColors";
interface Props {
closeBottomSheet: ( ) => void;
navAndCloseBottomSheet: ( screen: string, params?: {
camera?: string
} ) => void;
hidden: boolean;
}
type ObsCreateItem = {
text: string,
icon: string,
onPress: ( ) => void,
testID: string,
accessibilityLabel: string,
accessibilityHint: string
}
const majorVersionIOS = parseInt( String( Platform.Version ), 10 );
const AI_CAMERA_SUPPORTED = ( Platform.OS === "ios" && majorVersionIOS >= 11 )
|| ( Platform.OS === "android" && Platform.Version > 21 );
const GREEN_CIRCLE_CLASS = "bg-inatGreen rounded-full h-[36px] w-[36px] mb-2";
const ROW_CLASS = "flex-row justify-center space-x-4 w-full flex-1";
const AddObsBottomSheet = ( {
closeBottomSheet, navAndCloseBottomSheet, hidden
}: Props ) => {
const { t } = useTranslation( );
const prepareObsEdit = useStore( state => state.prepareObsEdit );
const obsCreateItems = useMemo( ( ) => ( {
aiCamera: {
text: t( "ID-with-AI-Camera" ),
icon: "aicamera",
onPress: ( ) => navAndCloseBottomSheet( "Camera", { camera: "AI" } ),
testID: "aicamera-button",
accessibilityLabel: t( "AI-Camera" ),
accessibilityHint: t( "Navigates-to-AI-camera" )
},
standardCamera: {
text: t( "Take-photos" ),
icon: "camera",
onPress: ( ) => navAndCloseBottomSheet( "Camera", { camera: "Standard" } ),
testID: "camera-button",
accessibilityLabel: t( "Camera" ),
accessibilityHint: t( "Navigates-to-camera" )
},
photoLibrary: {
text: t( "Upload-photos" ),
icon: "photo-library",
onPress: ( ) => navAndCloseBottomSheet( "PhotoLibrary" ),
testID: "import-media-button",
accessibilityLabel: t( "Photo-importer" ),
accessibilityHint: t( "Navigates-to-photo-importer" )
},
soundRecorder: {
text: t( "Record-a-sound" ),
icon: "microphone",
onPress: ( ) => navAndCloseBottomSheet( "SoundRecorder" ),
testID: "record-sound-button",
accessibilityLabel: t( "Sound-recorder" ),
accessibilityHint: t( "Navigates-to-sound-recorder" )
},
noEvidence: {
text: t( "Create-observation-with-no-evidence" ),
icon: "noevidence",
onPress: async ( ) => {
const newObservation = await Observation.new( );
prepareObsEdit( newObservation );
navAndCloseBottomSheet( "ObsEdit" );
},
testID: "observe-without-evidence-button",
accessibilityLabel: t( "Observation-with-no-evidence" ),
accessibilityHint: t( "Navigates-to-observation-edit-screen" )
}
} ), [
navAndCloseBottomSheet,
prepareObsEdit,
t
] );
const renderAddObsIcon = ( {
accessibilityHint,
accessibilityLabel,
icon,
onPress,
testID,
text
}: ObsCreateItem ) => (
<Pressable
className="bg-white w-1/2 flex-column items-center py-4 rounded-lg flex-1 shadow-sm
shadow-black/25 active:opacity-50"
onPress={onPress}
accessibilityHint={accessibilityHint}
accessibilityLabel={accessibilityLabel}
testID={testID}
>
<INatIconButton
className={GREEN_CIRCLE_CLASS}
accessibilityHint={accessibilityHint}
accessibilityLabel={accessibilityLabel}
color={String( colors?.white )}
icon={icon}
onPress={onPress}
size={icon === "aicamera"
? 28
: 20}
/>
<Body3>{text}</Body3>
</Pressable>
);
return (
<BottomSheet
onPressClose={closeBottomSheet}
hidden={hidden}
insideModal={false}
hideCloseButton
containerClass="bg-lightGray pt-4"
scrollEnabled={false}
>
<View className="flex-column gap-y-4 pb-4 px-4">
<View className={ROW_CLASS}>
{renderAddObsIcon( obsCreateItems.standardCamera )}
{renderAddObsIcon( obsCreateItems.photoLibrary )}
</View>
<View className={ROW_CLASS}>
{renderAddObsIcon( obsCreateItems.soundRecorder )}
{AI_CAMERA_SUPPORTED && renderAddObsIcon( obsCreateItems.aiCamera )}
</View>
<Pressable
className="bg-mediumGray w-full flex-row items-center py-[10px] px-5 rounded-lg
active:opacity-75"
onPress={obsCreateItems.noEvidence.onPress}
accessibilityHint={obsCreateItems.noEvidence.accessibilityHint}
accessibilityLabel={obsCreateItems.noEvidence.accessibilityLabel}
testID={obsCreateItems.noEvidence.testID}
>
<View className="mr-2">
<INatIcon
name={obsCreateItems.noEvidence.icon}
color={String( colors?.darkGray )}
size={24}
/>
</View>
<Body3>{obsCreateItems.noEvidence.text}</Body3>
</Pressable>
</View>
</BottomSheet>
);
};
export default AddObsBottomSheet;

View File

@@ -0,0 +1,199 @@
// @flow
import { CommonActions, useNavigation } from "@react-navigation/native";
import AddObsBottomSheet from "components/AddObsBottomSheet/AddObsBottomSheet";
import AddObsTooltip from "components/AddObsBottomSheet/AddObsTooltip";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import { navigationRef } from "navigation/navigationUtils";
import * as React from "react";
import { log } from "sharedHelpers/logger";
import { useCurrentUser, useLayoutPrefs, useTranslation } from "sharedHooks";
import useStore, { zustandStorage } from "stores/useStore";
const logger = log.extend( "AddObsButton" );
const AddObsButton = ( ): React.Node => {
const [showBottomSheet, setShowBottomSheet] = React.useState( false );
const [showTooltip, setShowTooltip] = React.useState( false );
const [currentRoute, setCurrentRoute] = React.useState( null );
const { t } = useTranslation();
const openBottomSheet = React.useCallback( () => setShowBottomSheet( true ), [] );
const closeBottomSheet = React.useCallback( () => setShowBottomSheet( false ), [] );
const { isAllAddObsOptionsMode } = useLayoutPrefs( );
const currentUser = useCurrentUser( );
// #region Tooltip logic
// Controls whether to show the tooltip, and to show it only once to the user
const showKey = "AddObsButtonTooltip";
const shownOnce = useStore( state => state.layout.shownOnce );
const setShownOnce = useStore( state => state.layout.setShownOnce );
const justFinishedSignup = useStore( state => state.layout.justFinishedSignup );
const numOfUserObservations = zustandStorage.getItem( "numOfUserObservations" );
const obsCountLoaded = typeof numOfUserObservations === "number";
React.useEffect( () => {
let timeoutId = null;
// We must know the route name and observation count before evaluating the triggerCondition
// And, the tooltip should only appear once per app download.
const shouldEvaluateTrigger = currentRoute?.name && obsCountLoaded && !shownOnce[showKey];
if ( shouldEvaluateTrigger ) {
// Base trigger condition in all cases:
// Only show the tooltip if the user has the AI camera as the default button option.
// Only show the tooltip on MyObservations screen.
const onObsList = currentRoute?.name === "ObsList";
const onlyAiCamera = !isAllAddObsOptionsMode;
let triggerCondition = onObsList && onlyAiCamera;
if ( justFinishedSignup ) {
// If a user creates a new account, they should see the tooltip right after dismissing the
// account creation pivot card and landing on My Obs.
triggerCondition = triggerCondition && !!shownOnce["account-creation"];
} else if ( !currentUser ) {
// If logged out, user should see the tooltip after making their second observation
triggerCondition = triggerCondition && numOfUserObservations > 1;
} else if ( numOfUserObservations > 50 ) {
// If a user logs in to an existing account with >50 observations, they should see the
// tooltip right after dismissing the "Welcome back!" pivot card and landing on My Obs.
triggerCondition = triggerCondition && !!shownOnce["fifty-observation"];
// If a user logs in to an existing account with <=50 observations,
// they should see the tooltip right after landing on My Obs after signing in
//
// If a user is already logged in and updates the app when tooltip is released,
// they should see the tooltip the first time they open the app after updating
//
// Both those cases are covered by not changing the base trigger condition.
}
// We use a timeout to avoid opening/closing two modals at the same time, such as the
// PivotCards that also appear on the MyObs screen.
if ( triggerCondition ) {
timeoutId = setTimeout( () => {
setShowTooltip( true );
}, 500 );
}
}
return () => {
if ( timeoutId != null ) {
clearTimeout( timeoutId );
}
};
}, [
currentRoute,
obsCountLoaded,
numOfUserObservations,
justFinishedSignup,
currentUser,
isAllAddObsOptionsMode,
shownOnce
] );
const dismissTooltip = () => {
setShowTooltip( false );
setShownOnce( showKey );
openBottomSheet();
};
// #endregion
// #region Navigation handling
const resetObservationFlowSlice = useStore( state => state.resetObservationFlowSlice );
const navigation = useNavigation( );
React.useEffect( ( ) => {
// don't remove this logger.info statement: it's used for internal
// metrics. isAdvancedUser name is vestigial, changing it will make it
// impossible to compare with older log data
logger.info( `isAdvancedUser: ${isAllAddObsOptionsMode}` );
}, [isAllAddObsOptionsMode] );
const navAndCloseBottomSheet = ( screen, params ) => {
if ( screen !== "ObsEdit" ) {
resetObservationFlowSlice( );
}
// we need to reset the navigation stack whenever a user navigates from the AddObs wheel,
// otherwise the user can end up closing out to a previous place in the stack, #1857
navigation.dispatch(
CommonActions.reset( {
index: 0,
routes: [
{
name: "NoBottomTabStackNavigator",
state: {
index: 0,
routes: [
{
name: screen,
params: { ...params, previousScreen: currentRoute }
}
]
}
}
]
} )
);
closeBottomSheet( );
};
const navToARCamera = ( ) => { navAndCloseBottomSheet( "Camera", { camera: "AI" } ); };
// #endregion
// Keeps currentRoute up-to-date for the use of both navigation & tooltip logic
React.useEffect( () => {
if ( navigationRef.isReady() ) {
const current = navigationRef.getCurrentRoute();
setCurrentRoute( current );
}
const unsubscribe = navigationRef.addListener( "state", () => {
const current = navigationRef.getCurrentRoute();
setCurrentRoute( current );
} );
return unsubscribe;
}, [] );
return (
<>
<AddObsTooltip isVisible={showTooltip} dismissTooltip={dismissTooltip} />
<AddObsBottomSheet
closeBottomSheet={closeBottomSheet}
hidden={!showBottomSheet}
navAndCloseBottomSheet={navAndCloseBottomSheet}
/>
<GradientButton
sizeClassName="w-[69px] h-[69px] mb-[5px]"
onLongPress={() => {
if ( !isAllAddObsOptionsMode ) openBottomSheet();
}}
onPress={() => {
if ( isAllAddObsOptionsMode ) {
openBottomSheet();
} else {
navToARCamera();
}
}}
accessibilityLabel={t( "Add-observations" )}
accessibilityHint={
isAllAddObsOptionsMode
? t( "Shows-observation-creation-options" )
: t( "Opens-AI-camera" )
}
iconName={isAllAddObsOptionsMode && "plus"}
iconSize={isAllAddObsOptionsMode && 31}
/>
</>
);
};
export default AddObsButton;

View File

@@ -0,0 +1,54 @@
import classNames from "classnames";
import { Body2, Modal } from "components/SharedComponents";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import { View } from "components/styledComponents";
import React from "react";
import { useTranslation } from "sharedHooks";
interface Props {
isVisible: boolean;
dismissTooltip: ( ) => void;
}
const AddObsTooltip = ( { isVisible, dismissTooltip }: Props ) => {
const { t } = useTranslation();
const modalContent = (
<View className="flex-1 bg-black/50 items-center justify-end">
<View className="relative bottom-[24px] items-center">
<View className="bg-white rounded-2xl px-5 py-4">
<Body2>{t( "Press-and-hold-to-view-more-options" )}</Body2>
</View>
<View
className={classNames(
"border-l-[10px] border-r-[10px] border-x-[#00000000]",
"border-t-[16px] border-t-white mb-2"
)}
/>
<GradientButton
sizeClassName="w-[69px] h-[69px]"
onPress={() => {}}
onLongPress={() => dismissTooltip()}
accessibilityLabel={t( "Add-observations" )}
accessibilityHint={t( "Shows-observation-creation-options" )}
/>
</View>
</View>
);
return (
<Modal
animationIn="fadeIn"
animationOut="fadeOut"
backdropOpacity={0}
closeModal={dismissTooltip}
disableSwipeDirection
fullScreen
modal={modalContent}
showModal={isVisible}
/>
);
};
export default AddObsTooltip;

View File

@@ -1,175 +0,0 @@
// @flow
import { CommonActions, useNavigation } from "@react-navigation/native";
import AddObsModal from "components/AddObsModal/AddObsModal";
import { Modal } from "components/SharedComponents";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import { t } from "i18next";
import { getCurrentRoute } from "navigation/navigationUtils";
import * as React from "react";
import { log } from "sharedHelpers/logger";
import { useCurrentUser, useLayoutPrefs } from "sharedHooks";
import useStore, { zustandStorage } from "stores/useStore";
const logger = log.extend( "AddObsButton" );
const AddObsButton = ( ): React.Node => {
const [showModal, setModal] = React.useState( false );
const openModal = React.useCallback( () => setModal( true ), [] );
const closeModal = React.useCallback( () => setModal( false ), [] );
const { isAllAddObsOptionsMode } = useLayoutPrefs( );
const currentRoute = getCurrentRoute( );
const currentUser = useCurrentUser( );
// Controls whether to show the tooltip, and to show it only once to the user
const showKey = "AddObsButtonTooltip";
const shownOnce = useStore( state => state.layout.shownOnce );
const setShownOnce = useStore( state => state.layout.setShownOnce );
const justFinishedSignup = useStore( state => state.layout.justFinishedSignup );
const numOfUserObservations = zustandStorage.getItem( "numOfUserObservations" );
// Base trigger condition in all cases:
// Only show the tooltip if the user has only AI camera as an option in this button.
// Only show the tooltip on MyObservations screen.
let triggerCondition = !isAllAddObsOptionsMode && currentRoute?.name === "ObsList";
if ( justFinishedSignup ) {
// If a user creates a new account, they should see the tooltip right after
// dismissing the account creation pivot card and landing on My Obs.
triggerCondition = triggerCondition && !!shownOnce["account-creation"];
} else if ( numOfUserObservations === undefined
|| numOfUserObservations === null
|| typeof numOfUserObservations !== "number" ) {
// If numOfUserObservations is undefined or null, we can not know if we should show the
// tooltip to the user. Usually this happens when the user logs in before making an
// observation, then we need to fetch the number of observations from server.
triggerCondition = false;
} else if ( !currentUser ) {
// If logged out, user should see the tooltip after making their second observation
// If a user is logged out, they should see the tooltip after making their second observation.
triggerCondition = triggerCondition && numOfUserObservations > 1;
} else if ( numOfUserObservations > 50 ) {
// If a user logs in to an existing account with <=50 observations,
// they should see the tooltip right after landing on My Obs after signing in
//
// If a user is already logged in and updates the app when tooltip is released,
// they should see the tooltip the first time they open the app after updating
//
// Both those cases are covered by not changing the base trigger condition.
//
// If a user logs in to an existing account with >50 observations, they should
// see the tooltip right after dismissing the "Welcome back!" pivot card
// and landing on My Obs.
triggerCondition = triggerCondition && !!shownOnce["fifty-observation"];
}
// The tooltip should only appear once per app download.
const tooltipIsVisible = !shownOnce[showKey] && triggerCondition;
React.useEffect( () => {
// If the tooltip visibility condition changes from false to true,
// we set the showModal state to true because the tooltip is in the modal.
// We have a lot of modals in the app, so we use a timeout to avoid opening two modals
// at the same time, like the PivotCards for example that in some cases were just closed
// by the user.
let timeoutId;
if ( tooltipIsVisible ) {
timeoutId = setTimeout( () => {
openModal();
}, 400 );
}
return () => {
clearTimeout( timeoutId );
};
}, [tooltipIsVisible, openModal] );
const resetObservationFlowSlice = useStore( state => state.resetObservationFlowSlice );
const navigation = useNavigation( );
React.useEffect( ( ) => {
// don't remove this logger.info statement: it's used for internal
// metrics. isAdvancedUser name is vestigial, changing it will make it
// impossible to compare with older log data
logger.info( `isAdvancedUser: ${isAllAddObsOptionsMode}` );
}, [isAllAddObsOptionsMode] );
const navAndCloseModal = ( screen, params ) => {
if ( screen !== "ObsEdit" ) {
resetObservationFlowSlice( );
}
// we need to reset the navigation stack whenever a user navigates from the AddObs wheel,
// otherwise the user can end up closing out to a previous place in the stack, #1857
navigation.dispatch(
CommonActions.reset( {
index: 0,
routes: [
{
name: "NoBottomTabStackNavigator",
state: {
index: 0,
routes: [
{
name: screen,
params: { ...params, previousScreen: currentRoute }
}
]
}
}
]
} )
);
closeModal( );
};
const navToARCamera = ( ) => { navAndCloseModal( "Camera", { camera: "AI" } ); };
const addObsModal = (
<AddObsModal
closeModal={closeModal}
navAndCloseModal={navAndCloseModal}
tooltipIsVisible={tooltipIsVisible}
dismissTooltip={( ) => {
if ( tooltipIsVisible ) setShownOnce( showKey );
}}
/>
);
return (
<>
{/* match the animation timing on FadeInView.tsx */}
<Modal
animationIn="fadeIn"
animationOut="fadeOut"
animationInTiming={250}
animationOutTiming={250}
showModal={showModal}
closeModal={tooltipIsVisible
? undefined
: closeModal}
modal={addObsModal}
/>
<GradientButton
sizeClassName="w-[69px] h-[69px] mb-[5px]"
onLongPress={() => {
if ( !isAllAddObsOptionsMode ) openModal();
}}
onPress={() => {
if ( isAllAddObsOptionsMode ) {
openModal();
} else {
navToARCamera();
}
}}
accessibilityLabel={t( "Add-observations" )}
accessibilityHint={
isAllAddObsOptionsMode
? t( "Shows-observation-creation-options" )
: t( "Opens-AI-camera" )
}
iconName={isAllAddObsOptionsMode && "plus"}
iconSize={isAllAddObsOptionsMode && 31}
/>
</>
);
};
export default AddObsButton;

View File

@@ -1,192 +0,0 @@
import classnames from "classnames";
import {
Body2,
INatIconButton
} from "components/SharedComponents";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import { View } from "components/styledComponents";
import React, { useMemo } from "react";
import { Platform, StatusBar } from "react-native";
import Observation from "realmModels/Observation";
import { useTranslation } from "sharedHooks";
import useStore from "stores/useStore";
import colors from "styles/tailwindColors";
import AddObsModalHelp, { ObsCreateItem } from "./AddObsModalHelp";
interface Props {
closeModal: ( ) => void;
navAndCloseModal: ( screen: string, params?: {
camera?: string
} ) => void;
tooltipIsVisible: boolean;
dismissTooltip: () => void;
}
const majorVersionIOS = parseInt( String( Platform.Version ), 10 );
const AI_CAMERA_SUPPORTED = ( Platform.OS === "ios" && majorVersionIOS >= 11 )
|| ( Platform.OS === "android" && Platform.Version > 21 );
const GREEN_CIRCLE_CLASS = "bg-inatGreen rounded-full h-[46px] w-[46px]";
const ROW_CLASS = "flex-row justify-center";
const MARGINS = AI_CAMERA_SUPPORTED
? {
standardCamera: "mr-[37px] bottom-[1px]",
photoLibrary: "ml-[37px] bottom-[1px]",
noEvidence: "mr-[26px]",
soundRecorder: "ml-[26px]"
}
: {
standardCamera: "mr-[9px]",
photoLibrary: "ml-[9px]",
noEvidence: "mr-[20px] bottom-[33px]",
soundRecorder: "ml-[20px] bottom-[33px]"
};
const AddObsModal = ( {
closeModal, navAndCloseModal, tooltipIsVisible, dismissTooltip
}: Props ) => {
const { t } = useTranslation( );
const prepareObsEdit = useStore( state => state.prepareObsEdit );
const obsCreateItems = useMemo( ( ) => ( {
aiCamera: {
text: t( "Use-iNaturalists-AI-Camera" ),
icon: "aicamera",
onPress: ( ) => navAndCloseModal( "Camera", { camera: "AI" } ),
testID: "aicamera-button",
className: classnames( GREEN_CIRCLE_CLASS, "absolute bottom-[26px]" ),
accessibilityLabel: t( "AI-Camera" ),
accessibilityHint: t( "Navigates-to-AI-camera" )
},
standardCamera: {
text: t( "Take-multiple-photos-of-a-single-organism" ),
icon: "camera",
onPress: ( ) => navAndCloseModal( "Camera", { camera: "Standard" } ),
testID: "camera-button",
accessibilityLabel: t( "Camera" ),
accessibilityHint: t( "Navigates-to-camera" ),
className: classnames( GREEN_CIRCLE_CLASS, MARGINS.standardCamera )
},
photoLibrary: {
text: t( "Upload-photos-from-your-photo-library" ),
icon: "photo-library",
onPress: ( ) => navAndCloseModal( "PhotoLibrary" ),
testID: "import-media-button",
className: classnames( GREEN_CIRCLE_CLASS, MARGINS.photoLibrary ),
accessibilityLabel: t( "Photo-importer" ),
accessibilityHint: t( "Navigates-to-photo-importer" )
},
soundRecorder: {
text: t( "Record-a-sound" ),
icon: "microphone",
onPress: ( ) => navAndCloseModal( "SoundRecorder" ),
testID: "record-sound-button",
className: classnames( GREEN_CIRCLE_CLASS, MARGINS.soundRecorder ),
accessibilityLabel: t( "Sound-recorder" ),
accessibilityHint: t( "Navigates-to-sound-recorder" )
},
noEvidence: {
text: t( "Create-an-observation-evidence" ),
icon: "noevidence",
onPress: async ( ) => {
const newObservation = await Observation.new( );
prepareObsEdit( newObservation );
navAndCloseModal( "ObsEdit" );
},
testID: "observe-without-evidence-button",
className: classnames( GREEN_CIRCLE_CLASS, MARGINS.noEvidence ),
accessibilityLabel: t( "Observation-with-no-evidence" ),
accessibilityHint: t( "Navigates-to-observation-edit-screen" )
},
closeButton: {
testID: "close-camera-options-button",
icon: "close",
className: classnames( GREEN_CIRCLE_CLASS, "h-[69px] w-[69px]" ),
onPress: closeModal,
accessibilityLabel: t( "Close" ),
accessibilityHint: t( "Closes-new-observation-options" )
}
} ), [
closeModal,
navAndCloseModal,
prepareObsEdit,
t
] );
const renderAddObsIcon = ( {
accessibilityHint,
accessibilityLabel,
className,
icon,
onPress,
testID
}: ObsCreateItem ) => (
<INatIconButton
accessibilityHint={accessibilityHint}
accessibilityLabel={accessibilityLabel}
className={className}
color={String( colors?.white )}
icon={icon}
onPress={onPress}
size={icon === "aicamera"
? 38
: 30}
testID={testID}
/>
);
const renderContent = ( ) => {
if ( tooltipIsVisible ) {
return (
<View className="justify-center items-center">
<View className="bg-white rounded-2xl px-5 py-4">
<Body2>{t( "Press-and-hold-to-view-more-options" )}</Body2>
</View>
<View
className={classnames(
// I could not figure out how to use "border-x-transparent",
"border-l-[10px] border-r-[10px] border-x-[#00000000]",
"border-t-[16px] border-t-white mb-2"
)}
/>
<GradientButton
sizeClassName="w-[69px] h-[69px]"
onPress={() => {}}
onLongPress={() => dismissTooltip( )}
accessibilityLabel={t( "Add-observations" )}
accessibilityHint={t( "Shows-observation-creation-options" )}
/>
</View>
);
}
return (
<>
<AddObsModalHelp obsCreateItems={obsCreateItems} />
<View className={classnames( ROW_CLASS, {
"bottom-[20px]": !AI_CAMERA_SUPPORTED
} )}
>
{renderAddObsIcon( obsCreateItems.standardCamera )}
{AI_CAMERA_SUPPORTED && renderAddObsIcon( obsCreateItems.aiCamera )}
{renderAddObsIcon( obsCreateItems.photoLibrary )}
</View>
<View className={classnames( ROW_CLASS, "items-center" )}>
{renderAddObsIcon( obsCreateItems.noEvidence )}
{renderAddObsIcon( obsCreateItems.closeButton )}
{renderAddObsIcon( obsCreateItems.soundRecorder )}
</View>
</>
);
};
return (
<>
<StatusBar barStyle="light-content" backgroundColor="black" />
{ renderContent( ) }
</>
);
};
export default AddObsModal;

View File

@@ -1,116 +0,0 @@
import classnames from "classnames";
import {
Body3,
Heading2,
INatIcon,
INatIconButton
} from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import React, { useState } from "react";
import { useDeviceOrientation, useTranslation } from "sharedHooks";
import { storage } from "stores/useStore";
import colors from "styles/tailwindColors";
export type ObsCreateItem = {
text?: string,
icon: string,
onPress: ( ) => void,
testID: string,
className: string,
accessibilityLabel: string,
accessibilityHint: string
}
type Props = {
obsCreateItems: {
[addType: string]: ObsCreateItem
}
};
const HIDE_ADD_OBS_HELP_TEXT = "hideAddObsHelpText";
const AddObsModalHelp = ( {
obsCreateItems
}: Props ) => {
const { t } = useTranslation( );
const { screenHeight } = useDeviceOrientation( );
const [hideHelpText, setHideHelpText] = useState( storage.getBoolean( HIDE_ADD_OBS_HELP_TEXT ) );
// targeting iPhone SE, which has height of 667
const isSmallScreen = screenHeight < 670;
if ( hideHelpText ) return null;
return (
<View
className={classnames( "bg-white rounded-3xl py-[23px] mb-20", {
"py-[5px] mb-10": isSmallScreen
} )}
>
<View className={classnames( "flex-row items-center mb-2" )}>
<Heading2
maxFontSizeMultiplier={1.5}
testID="identify-text"
className={classnames( "pl-[25px]", {
"px-8 -mb-2 mt-2": isSmallScreen
} )}
>
{t( "Identify-an-organism" )}
</Heading2>
<View className={classnames( "ml-auto pr-[12px]", {
"pb-6": isSmallScreen
} )}
>
<INatIconButton
icon="close"
color={String( colors?.darkGray )}
size={19}
onPress={async ( ) => {
setHideHelpText( true );
storage.set( HIDE_ADD_OBS_HELP_TEXT, true );
}}
accessibilityLabel={t( "Close" )}
accessibilityHint={t( "Closes-new-observation-explanation" )}
/>
</View>
</View>
<View className={classnames( "px-[23px]", {
"px-[10px]": isSmallScreen
} )}
>
{Object.keys( obsCreateItems )
.filter( k => k !== "closeButton" )
.map( k => {
const item = obsCreateItems[k];
return (
<Pressable
accessibilityRole="button"
className={classnames( "flex-row items-center p-2 my-1", {
"p-1": isSmallScreen
} )}
key={k}
onPress={item.onPress}
>
<INatIcon
name={item.icon}
size={item.icon === "aicamera"
? 30
: 26}
color={String(
item.icon === "aicamera"
? colors?.inatGreen
: colors?.darkGray
)}
/>
<Body3 maxFontSizeMultiplier={1.5} className="ml-[20px] shrink">
{item.text}
</Body3>
</Pressable>
);
} )}
</View>
</View>
);
};
export default AddObsModalHelp;

View File

@@ -12,7 +12,8 @@ const logger = log.extend( "AppStateListener" );
const AppStateListener = ( ) => {
const { loadTime } = usePerformance( {
screenName: "AppStateListener"
screenName: "AppStateListener",
isLoading: false
} );
if ( isDebugMode( ) ) {
logger.info( loadTime );

View File

@@ -1,4 +1,4 @@
import AddObsButton from "components/AddObsModal/AddObsButton";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import {
Body1,
Body2,

View File

@@ -23,7 +23,7 @@ type Props = {
const LoginBanner = ( {
currentUser
}: Props ): React.ReactNode => {
}: Props ) => {
const { t } = useTranslation( );
const navigation = useNavigation();
const loginBannerDismissed = useStore( state => state.layout.loginBannerDismissed );

View File

@@ -1,6 +1,5 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import AddObsModal from "components/AddObsModal/AddObsModal";
import { AccountCreationCard } from "components/OnboardingModal/PivotCards";
import {
HeaderUser,
@@ -8,15 +7,13 @@ import {
ViewWrapper
} from "components/SharedComponents";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import Modal from "components/SharedComponents/Modal";
import {
Pressable, View
} from "components/styledComponents";
import Arrow from "images/svg/curved_arrow_down.svg";
import type { Node } from "react";
import React, { useState } from "react";
import React from "react";
import { useTranslation } from "sharedHooks";
import useStore from "stores/useStore";
import LoginBanner from "./LoginBanner";
@@ -30,20 +27,13 @@ const MyObservationsEmptySimple = ( { currentUser, isConnected, justFinishedSign
Props ): Node => {
const { t } = useTranslation();
const navigation = useNavigation();
const [showModal, setShowModal] = useState( false );
const resetObservationFlowSlice = useStore( state => state.resetObservationFlowSlice );
const navAndCloseModal = ( screen, params ) => {
if ( screen !== "ObsEdit" ) {
resetObservationFlowSlice( );
}
const navToARCamera = ( ) => {
navigation.navigate( "NoBottomTabStackNavigator", {
screen,
params
screen: "Camera",
params: { camera: "AI" }
} );
setShowModal( false );
};
const navToARCamera = ( ) => { navAndCloseModal( "Camera", { camera: "AI" } ); };
return (
<>
@@ -77,16 +67,6 @@ const MyObservationsEmptySimple = ( { currentUser, isConnected, justFinishedSign
/>
</View>
</View>
<Modal
showModal={showModal}
closeModal={( ) => setShowModal( false )}
modal={(
<AddObsModal
closeModal={( ) => setShowModal( false )}
navAndCloseModal={navAndCloseModal}
/>
)}
/>
</ViewWrapper>
<AccountCreationCard

View File

@@ -1,15 +1,13 @@
// @flow
import { Body4 } from "components/SharedComponents";
import { t } from "i18next";
import type { Node } from "react";
import React from "react";
import type { License, RealmObservation } from "realmModels/types";
type Props = {
observation: Object
interface Props {
observation: RealmObservation;
}
const renderRestrictions = ( licenseCode: string ) => {
const renderRestrictions = ( licenseCode: License | null ) => {
switch ( licenseCode ) {
case "cc0":
return t( "no-rights-reserved-cc0" );
@@ -32,7 +30,7 @@ const renderRestrictions = ( licenseCode: string ) => {
// lifted from web:
// https://github.com/inaturalist/inaturalist/blob/768b9263931ebeea229bbc47d8442ca6b0377d45/app/webpack/shared/components/observation_attribution.jsx
const Attribution = ( { observation }: Props ): Node => {
const Attribution = ( { observation }: Props ) => {
const { user } = observation;
const userName = user
? ( user.name || user.login )

View File

@@ -16,10 +16,18 @@ import AudioRecorderPlayer from "react-native-audio-recorder-player";
import { useTranslation } from "sharedHooks";
import colors from "styles/tailwindColors";
const SoundSlider = ( { playBackState, onSlidingComplete } ) => {
interface SoundSliderProps {
playBackState: {
currentPosition: number;
duration: number;
};
onSlidingComplete: ( value: number ) => void;
}
const SoundSlider = ( { playBackState, onSlidingComplete }: SoundSliderProps ) => {
const sliderStyle = {
width: "100%"
};
} as const;
return (
<Slider
style={sliderStyle}
@@ -37,12 +45,21 @@ const SoundSlider = ( { playBackState, onSlidingComplete } ) => {
);
};
interface SoundContainerProps {
autoPlay: boolean;
isVisible: boolean;
sizeClass: string;
sound: {
file_url: string;
};
}
const SoundContainer = ( {
autoPlay,
isVisible,
sizeClass,
sound
} ) => {
}: SoundContainerProps ) => {
const needsInternet = sound.file_url.includes( "https://" );
const { isConnected } = useNetInfo( );
const playerRef = useRef( new AudioRecorderPlayer( ) );
@@ -68,14 +85,14 @@ const SoundContainer = ( {
Math.floor( value / 1000 )
), [player] );
const playSound = useCallback( position => {
const playSound = useCallback( ( position?: number ) => {
async function playSoundAsync( ) {
await player.startPlayer( sound.file_url );
if ( position ) {
try {
await player.seekToPlayer( position );
} catch ( seekPlayerError ) {
if ( seekPlayerError.message.match( /Player has already stopped/ ) ) {
if ( seekPlayerError instanceof Error && seekPlayerError.message.match( /Player has already stopped/ ) ) {
// Something else might be wrong, but it's not really something to
// bother the user with
return;
@@ -110,7 +127,7 @@ const SoundContainer = ( {
try {
await player.pausePlayer( );
} catch ( pausePlayerError ) {
if ( pausePlayerError.message.match( /Player has already stopped/ ) ) {
if ( pausePlayerError instanceof Error && pausePlayerError.message.match( /Player has already stopped/ ) ) {
// Something else might be wrong, but it's not really something to
// bother the user with
return;

View File

@@ -54,7 +54,7 @@ const SlideItem = props => {
height={item.iconProps.height}
/>
)}
<Heading1 className="text-white mt-[30px]">
<Heading1 className="text-white mt-[30px] text-center">
{item.title}
</Heading1>
<Body1 className="text-center text-white mt-[20px]">

View File

@@ -63,7 +63,7 @@ const PhotoSharing = ( ) => {
const { data } = item;
// when sharing, we need to reset zustand like we do while
// navigating through the AddObsModal
// navigating through the AddObsBottomSheet
resetObservationFlowSlice( );
const photoUris = data

View File

@@ -25,7 +25,7 @@ const Settings = ( ) => {
setIsDefaultMode
} = useLayoutPrefs( );
const handleValueChange = useCallback( newValue => {
const handleValueChange = useCallback( ( newValue: boolean ) => {
setIsDefaultMode( !newValue );
}, [setIsDefaultMode] );

View File

@@ -13,9 +13,32 @@ interface ButtonProps {
style?: object;
}
const styles = StyleSheet.create( {
button: {
fontSize: 16,
margin: 16
},
container: {
flex: 1,
backgroundColor: "white"
},
text: {
fontSize: 20,
textAlign: "center",
margin: 16
},
buttonGroup: {
flexDirection: "row",
justifyContent: "space-around",
alignItems: "center"
},
destructive: {
color: "red"
}
} );
const Button = ( { onPress, title, style }: ButtonProps ) => (
<Pressable accessibilityRole="button" onPress={onPress}>
{/* eslint-disable-next-line no-use-before-define */}
<Text style={[styles.button, style]}>{title}</Text>
</Pressable>
);
@@ -32,7 +55,6 @@ const ShareSheet = () => {
const {
container, text, buttonGroup, destructive
// eslint-disable-next-line no-use-before-define
} = styles;
return (
@@ -59,28 +81,4 @@ const ShareSheet = () => {
);
};
const styles = StyleSheet.create( {
button: {
fontSize: 16,
margin: 16
},
container: {
flex: 1,
backgroundColor: "white"
},
text: {
fontSize: 20,
textAlign: "center",
margin: 16
},
buttonGroup: {
flexDirection: "row",
justifyContent: "space-around",
alignItems: "center"
},
destructive: {
color: "red"
}
} );
export default ShareSheet;

View File

@@ -1,13 +1,12 @@
import classnames from "classnames";
import { View } from "components/styledComponents";
import React from "react";
import React, { type PropsWithChildren } from "react";
import { useWindowDimensions } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const FOOTER_HEIGHT = 77;
interface Props {
children: React.ReactNode;
interface Props extends PropsWithChildren {
containerClassName?: string;
headerHeight: number;
emptyItemHeight: number;

View File

@@ -1,16 +1,15 @@
import classnames from "classnames";
import { INatIconButton } from "components/SharedComponents";
import { View } from "components/styledComponents";
import type { Node } from "react";
import React, { useCallback } from "react";
import { FlatList } from "react-native";
import { FlatList, ListRenderItemInfo } from "react-native";
import { useTranslation } from "sharedHooks";
import colors from "styles/tailwindColors";
type Props = {
before?: Node;
before?: React.ReactNode;
chosen: string[];
onTaxonChosen: Function;
onTaxonChosen: ( taxon: string ) => void;
testID?: string;
withoutUnknown?: boolean;
};
@@ -42,12 +41,12 @@ const IconicTaxonChooser = ( {
onTaxonChosen,
testID,
withoutUnknown
}: Props ): Node => {
}: Props ) => {
const { t } = useTranslation( );
const iconicTaxonIcons = withoutUnknown
? ICONIC_TAXA.filter( taxon => taxon !== "unknown" )
: ICONIC_TAXA;
const renderIcon = useCallback( ( { item: iconicTaxonName } ) => {
const renderIcon = useCallback( ( { item: iconicTaxonName }: ListRenderItemInfo<string> ) => {
const isSelected = chosen.indexOf( iconicTaxonName ) >= 0;
return (
<View
@@ -71,7 +70,9 @@ const IconicTaxonChooser = ( {
onPress={( ) => {
onTaxonChosen( iconicTaxonName );
}}
color={isSelected && colors.white}
color={isSelected
? colors.white
: undefined}
accessibilityLabel={
t( "Iconic-taxon-name", { iconicTaxon: iconicTaxonName } )
}

View File

@@ -1,13 +1,12 @@
import classNames from "classnames";
import { INatIcon } from "components/SharedComponents";
import { View } from "components/styledComponents";
import * as React from "react";
import React, { type PropsWithChildren } from "react";
type Props = {
icon: string,
size?: number,
classNameMargin?: string,
children: React.ReactNode
interface Props extends PropsWithChildren {
icon: string;
size?: number;
classNameMargin?: string;
}
const ContentWithIcon = ( {

View File

@@ -18,7 +18,10 @@ const { width } = Dimensions.get( "window" );
const marginOnWide = {
marginHorizontal: width > 500
? ( width - 500 ) / 2
: 0
: 0,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
overflow: "hidden"
};
// eslint-disable-next-line
@@ -29,14 +32,16 @@ interface Props {
hidden?: boolean;
hideCloseButton?: boolean;
headerText?: string;
onLayout?: Function;
onLayout?: ( event: object ) => void;
// Callback when the user presses the close button or backdrop, not whenever the sheet
// closes
onPressClose?: Function;
onPressClose?: ( ) => void;
snapPoints?: Array<string>;
insideModal?: boolean;
keyboardShouldPersistTaps?: string;
testID?: string;
containerClass?: string;
scrollEnabled?: boolean;
}
const StandardBottomSheet = ( {
@@ -49,7 +54,9 @@ const StandardBottomSheet = ( {
snapPoints,
insideModal,
keyboardShouldPersistTaps = "never",
testID
containerClass,
testID,
scrollEnabled = true
}: Props ): Node => {
if ( snapPoints ) {
throw new Error( "BottomSheet does not accept snapPoints as a prop." );
@@ -109,27 +116,33 @@ const StandardBottomSheet = ( {
>
<BottomSheetScrollView
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
scrollEnabled={scrollEnabled}
>
<View
className={classnames(
"pt-7",
insets.bottom > 0
? "pb-7"
: null
: null,
containerClass
)}
onLayout={onLayout}
// Not ideal, but @gorhom/bottom-sheet components don't support
// testID
testID={testID}
>
<View className="mx-12 flex">
<Heading4
testID="bottom-sheet-header"
className="w-full text-center"
>
{headerText}
</Heading4>
</View>
{!headerText
? null
: (
<View className="mx-12 flex">
<Heading4
testID="bottom-sheet-header"
className="w-full text-center"
>
{headerText}
</Heading4>
</View>
)}
{children}
{!hideCloseButton && (
<INatIconButton

View File

@@ -228,9 +228,6 @@ Closes-explanation = Closes explanation
# appear when you first install the app
Closes-introduction = Closes introduction
# Accessibility hint for button that closes the help that
# appears when you start a new observation for the first time
Closes-new-observation-explanation = Closes new observation explanation.
Closes-new-observation-options = Closes new observation options.
Closes-withdraw-id-sheet = Closes "Withdraw ID" sheet
# Heading for a section that describes people and organizations that
# collaborate with iNaturalist
@@ -274,7 +271,7 @@ Couldnt-create-comment = Couldn't create comment
Couldnt-create-identification-error = Couldn't create identification { $error }
Couldnt-create-identification-unknown-error = Couldn't create identification, unknown error.
CREATE-AN-ACCOUNT = CREATE AN ACCOUNT
Create-an-observation-evidence = Create an observation with no evidence
Create-observation-with-no-evidence = Create observation with no evidence
DATA-QUALITY = DATA QUALITY
DATA-QUALITY-ASSESSMENT = DATA QUALITY ASSESSMENT
# Label for button that navigates users to the data quality screen
@@ -563,6 +560,7 @@ Iconic-taxon-name = Iconic taxon name: { $iconicTaxon }
ID-Suggestions = ID Suggestions
# Short for: Identify with AI. Label for a button that will load identifications for a given photo/sound
ID-WITH-AI = ID WITH AI
ID-with-AI-Camera = ID with AI Camera
# Identification Status
ID-Withdrawn = ID Withdrawn
IDENTIFICATION = IDENTIFICATION
@@ -575,7 +573,6 @@ IDENTIFICATIONS-WITHOUT-NUMBER =
}
Identifiers = Identifiers
Identifiers-View = Identifiers View
Identify-an-organism = Identify an organism
# Title of screen asking for permission to access the camera
Identify-organisms-in-real-time-with-your-camera = Identify organisms in real time with your camera
# Onboarding slides
@@ -1210,8 +1207,8 @@ Switches-to-tab = Switches to { $tab } tab.
Sync-observations = Sync observations
Syncing = Syncing...
# Help text for the button that opens the multi-capture camera
Take-multiple-photos-of-a-single-organism = Take multiple photos of a single organism
Take-photo = Take photo
Take-photos = Take photos
# label in project requirements
Taxa = Taxa
TAXON = TAXON
@@ -1279,7 +1276,7 @@ Unreviewed-observations-only = Unreviewed observations only
Upload-Complete = Upload Complete
Upload-in-progress = Upload in progress
UPLOAD-NOW = UPLOAD NOW
Upload-photos-from-your-photo-library = Upload multiple photos from your photo library
Upload-photos = Upload photos
Upload-Progress = Upload { $uploadProgress } percent complete
UPLOAD-TO-INATURALIST = UPLOAD TO INATURALIST
# Shows the number of observations a user can upload to iNat from my observations page
@@ -1301,7 +1298,6 @@ Uploading-x-of-y-observations =
*[other] Uploading { $currentUploadCount } of { $total } observations
}
Use-iNaturalist-to-identify-any-living-thing = Use iNaturalist to identify any living thing
Use-iNaturalists-AI-Camera = Use iNaturalist's AI Camera to identify organisms in real time
# Text for a button prompting the user to grant access to location
USE-LOCATION = USE LOCATION
Use-the-devices-other-camera = Use the device's other camera.

View File

@@ -116,8 +116,6 @@
"Close-search": "Close search",
"Closes-explanation": "Closes explanation",
"Closes-introduction": "Closes introduction",
"Closes-new-observation-explanation": "Closes new observation explanation.",
"Closes-new-observation-options": "Closes new observation options.",
"Closes-withdraw-id-sheet": "Closes \"Withdraw ID\" sheet",
"COLLABORATORS": "COLLABORATORS",
"Collection-Project": "Collection Project",
@@ -144,7 +142,7 @@
"Couldnt-create-identification-error": "Couldn't create identification { $error }",
"Couldnt-create-identification-unknown-error": "Couldn't create identification, unknown error.",
"CREATE-AN-ACCOUNT": "CREATE AN ACCOUNT",
"Create-an-observation-evidence": "Create an observation with no evidence",
"Create-observation-with-no-evidence": "Create observation with no evidence",
"DATA-QUALITY": "DATA QUALITY",
"DATA-QUALITY-ASSESSMENT": "DATA QUALITY ASSESSMENT",
"Data-Quality-Assessment": "Data Quality Assessment",
@@ -315,13 +313,13 @@
"Iconic-taxon-name": "Iconic taxon name: { $iconicTaxon }",
"ID-Suggestions": "ID Suggestions",
"ID-WITH-AI": "ID WITH AI",
"ID-with-AI-Camera": "ID with AI Camera",
"ID-Withdrawn": "ID Withdrawn",
"IDENTIFICATION": "IDENTIFICATION",
"Identification-options": "Identification options",
"IDENTIFICATIONS-WITHOUT-NUMBER": "{ $count ->\n [one] IDENTIFICATION\n *[other] IDENTIFICATIONS\n}",
"Identifiers": "Identifiers",
"Identifiers-View": "Identifiers View",
"Identify-an-organism": "Identify an organism",
"Identify-organisms-in-real-time-with-your-camera": "Identify organisms in real time with your camera",
"Identify-species-anywhere": "Identify species anywhere",
"If-an-account-with-that-email-exists": "If an account with that email exists, we've sent password reset instructions to your email.",
@@ -762,8 +760,8 @@
"Switches-to-tab": "Switches to { $tab } tab.",
"Sync-observations": "Sync observations",
"Syncing": "Syncing...",
"Take-multiple-photos-of-a-single-organism": "Take multiple photos of a single organism",
"Take-photo": "Take photo",
"Take-photos": "Take photos",
"Taxa": "Taxa",
"TAXON": "TAXON",
"TAXON-NAMES-DISPLAY": "TAXON NAMES DISPLAY",
@@ -818,7 +816,7 @@
"Upload-Complete": "Upload Complete",
"Upload-in-progress": "Upload in progress",
"UPLOAD-NOW": "UPLOAD NOW",
"Upload-photos-from-your-photo-library": "Upload multiple photos from your photo library",
"Upload-photos": "Upload photos",
"Upload-Progress": "Upload { $uploadProgress } percent complete",
"UPLOAD-TO-INATURALIST": "UPLOAD TO INATURALIST",
"Upload-x-observations": "Upload { $count ->\n [one] 1 observation\n *[other] { $count } observations\n}",
@@ -827,7 +825,6 @@
"Uploading-x-of-y": "Uploading { $currentUploadCount } of { $total }",
"Uploading-x-of-y-observations": "{ $total ->\n [one] Uploading { $currentUploadCount } observation\n *[other] Uploading { $currentUploadCount } of { $total } observations\n}",
"Use-iNaturalist-to-identify-any-living-thing": "Use iNaturalist to identify any living thing",
"Use-iNaturalists-AI-Camera": "Use iNaturalist's AI Camera to identify organisms in real time",
"USE-LOCATION": "USE LOCATION",
"Use-the-devices-other-camera": "Use the device's other camera.",
"Use-the-iNaturalist-camera-to-see-real-time-identifications-and-take-photos": "Use the iNaturalist camera to see real-time identifications and take photos!",

View File

@@ -57,7 +57,7 @@ ALL-USERS-EXCEPT = SEMUA PENGGUNA KECUALI
ALLOW-LOCATION-ACCESS = IZINKAN AKSES LOKASI
Already-have-an-iNaturalist-account = Sudah punya akun iNaturalist?
An-Internet-connection-is-required = Koneksi Internet diperlukan untuk memuat lebih banyak pengamatan.
Analyzing-for-the-best-identification = Analyzing for the best identification...
Analyzing-for-the-best-identification = Menganalisis untuk identifikasi paling cocok...
Any--date = Semua
Any--establishment-means = Semua
Any--media-type = Semua
@@ -226,7 +226,7 @@ DETAILS = INFORMASI
Device-storage-full = Penyimpanan perangkat penuh
Device-storage-full-description = iNaturalist mungkin tidak bisa menyimpan gambar-gambar Anda atau dapat heng.
Disable-flash = Matikan kilat
Disable-location = Disable location
Disable-location = Nonaktifkan lokasi
Disagreement = *@{ $username } tidak setuju bahwa ini merupakan <0/>
DISCARD = HAPUS
DISCARD-ALL = HAPUS SEMUA
@@ -263,7 +263,7 @@ EDUCATORS = TENAGA PENDIDIK
EMAIL = EMAIL
EMAIL-DEBUG-LOGS = EMAIL LOG DEBUG
Enable-flash = Aktifkan kilat
Enable-location = Enable location
Enable-location = Aktifkan lokasi
Enable-notifications = Aktifkan pemberitahuan
Endemic = Endemik
Endemic-to-place = Endemik di { $place }
@@ -307,7 +307,7 @@ Filter-by-uploaded-between-dates = Saring berdasarkan pengamatan yang diunggah d
Filter-by-uploaded-on-date = Saring berdasarkan pengamatan yang diunggah pada tanggal tertentu
Filters = Saringan
Flagged = Telah Ditandai
Flash = Flash
Flash = Kilat
Flip-camera = Balikkan kamera
FOLLOW = IKUTI
FOLLOWING-X-PEOPLE =
@@ -325,8 +325,8 @@ Go-back = Kembali
Google-Play-Services-Not-Installed = Layanan Google Play Belum Terpasang
GRANT-PERMISSION = BERI IZIN
Grant-Permission-title = Berikan Izin
Group-Photos = Group Photos
Group-photos-onboarding = Group photos into observations make sure there is only one species per observation
Group-Photos = Kelompokkan Gambar
Group-photos-onboarding = Kelompokkan gambar menjadi pengamatan-pengamatannya pastikan hanya ada satu spesies per pengamatan
HELP = BANTUAN
Help-create-Research-Grade-data-used-in-science-and-conservation = Ikut membuat data Kelas Riset yang digunakan dalam sains dan konservasi.
Help-protect-species = Bantu melindungi spesies
@@ -362,7 +362,7 @@ If-you-want-to-collate-compare-promote = Jika Anda ingin mengumpulkan, membandin
If-youre-seeing-this-error = Jika Anda melihat pesan ini dan Anda sedang aktif, staf iNat telah diberitahu. Terima kasih telah menemukan bug! Jika Anda sedang tidak aktif, silakan mengambil tangkapan layar Anda dan kirimkan email kepada kami saat Anda sudah kembali aktif.
IGNORE-LOCATION = ABAIKAN LOKASI
Ignore-notifications = Abaikan pemberitahuan
Ignoring-location = Ignoring location
Ignoring-location = Mengabaikan lokasi
Import-Photos-From = Impor Gambar Dari
IMPORT-X-OBSERVATIONS =
IMPOR { $count ->
@@ -484,7 +484,7 @@ Navigates-to-AI-camera = Pergi ke kamera AI
Navigates-to-bulk-importer = Pergi ke pengimpor massal
Navigates-to-camera = Pergi ke kamera
Navigates-to-explore = Pergi ke jelajah
Navigates-to-match-screen = Navigates to match screen
Navigates-to-match-screen = Pergi ke layar kecocokan
Navigates-to-notifications = Pergi ke notifikasi
Navigates-to-observation-details = Pergi ke layar informasi pengamatan
Navigates-to-observation-edit-screen = Pergi ke layar edit pengamatan
@@ -550,13 +550,13 @@ OBSERVATIONS-WITHOUT-NUMBER =
*[other] PENGAMATAN
}
OBSERVE-ORGANISMS = AMATI ORGANISME
OBSERVED-AT--label = OBSERVED AT
OBSERVED-AT--label = DIAMATI PADA
OBSERVED-IN--label = DIAMATI DI
Observers = Pengamat
Observers-View = Tampilan Pengamat
October = Oktober
Offline-DQA-description = AKD belum akurat. Periksa koneksi internet Anda dan coba lagi.
Offline-suggestions-may-differ-from-online = Offline suggestions may differ from online suggestions, and taxon images and common names may not load.
Offline-suggestions-may-differ-from-online = Rekomendasi luring bisa berbeda dengan rekomendasi daring. Gambar takson serta nama umumnya mungkin tidak termuat.
OK = OK
Oldest-to-newest = Terlama ke terbaru
Once-you-create-and-upload-observations = Saat Anda membuat & mengunggah pengamatan, anggota lain dari komunitas kami dapat menambahkan identifikasi untuk membantu pengamatan Anda mencapai kelas riset.
@@ -774,7 +774,7 @@ Sign-in-with-Apple = Masuk dengan Apple
Sign-in-with-Apple-Failed = Gagal masuk dengan Apple
Sign-in-with-Google = Masuk dengan Google
Sign-in-with-Google-Failed = Gagal Masuk dengan Google
Skip-additional-suggestions = Skip additional suggestions
Skip-additional-suggestions = Lewati saran tambahan
Skip-for-now = Lewati untuk sekarang
Something-went-wrong = Terjadi kesalahan.
Sorry-this-observation-was-deleted = Maaf, pengamatan ini sudah dihapus
@@ -842,7 +842,7 @@ There-was-an-error-that-might-be-fixed-by-logging-in-again = Terjadi kesalahan y
This-is-a-wild-organism = Ini adalah organisme liar dan tidak ditaruh di lokasi ini oleh manusia.
This-is-how-taxon-names-will-be-displayed = Ini adalah pengaturan bagaimana semua nama takson akan ditampilkan kepada Anda di iNaturalist:
This-is-your-identification-other-people-may-help-confirm-it = Ini adalah identifikasi Anda. Orang lain dapat membantu Anda mengonfirmasinya.
This-may-take-a-few-seconds = This may take a few seconds.
This-may-take-a-few-seconds = Proses ini dapat memerlukan beberapa detik.
This-observation-has-no-comments-or-identifications-yet = Pengamatan ini belum memiliki komentar atau identifikasi.
This-observation-has-not-met-the-conditions-required-to-meet-Research-Grade = Pengamatan ini belum memenuhi persyaratan yang dibutuhkan untuk memenuhi status Kelas Riset.
This-observation-is-not-eligible-for-research-grade-status = Pengamatan ini belum memenuhi syarat untuk mendapatkan status kelas riset. Pelajari lebih lanjut di Penilaian Kualitas Data di bawah.
@@ -851,9 +851,9 @@ This-observation-needs-more-identifications = Pengamatan ini memerlukan lebih ba
This-observation-needs-more-identifications-to-become-research-grade = Pengamatan ini memerlukan lebih banyak identifikasi untuk mendapatkan kelas riset.
This-observer-has-opted-out-of-the-Community-Taxon = Pengamat ini telah memilih untuk keluar dari Takson Komunitas
This-organism-was-placed-by-humans = Organisme ini diperkenalkan ke tempat ini oleh manusia. Ini berlaku untuk makhluk-makhluk seperti tumbuhan di taman, hewan peliharaan, dan hewan-hewan di kebun binatang.
This-user-has-no-followers = This user has no followers.
This-user-has-not-joined-any-projects = This user has not joined any projects.
This-user-is-not-following-anyone = This user is not following anyone.
This-user-has-no-followers = Pengguna ini tidak memiliki pengikut.
This-user-has-not-joined-any-projects = Pengguna ini belum bergabung dengan proyek apa pun.
This-user-is-not-following-anyone = Pengguna ini tidak mengikuti siapa pun.
To-sync-your-observations-to-iNaturalist = Untuk menyinkronkan pengamatan-pengamatan Anda ke iNaturalist, harap masuk terlebih dahulu.
To-view-nearby-organisms-please-enable-location = Untuk melihat organisme-organisme di sekitar Anda, harap aktifkan lokasi.
To-view-nearby-projects-please-enable-location = Untuk melihat proyek di sekitar Anda, harap aktifkan lokasi.
@@ -898,7 +898,7 @@ USER = PENGGUNA
User = Pengguna { $userHandle }
USERNAME-OR-EMAIL = NAMA PENGGUNA ATAU EMAIL
Users = Pengguna
Using-location = Using location
Using-location = Menggunakan lokasi
Verified-IDs-are-used-for-science-and-conservation = ID terverifikasi digunakan untuk sains dan konservasi
Version-app-build = Versi { $appVersion } ({ $buildVersion })
VIEW-ALL-X-PLACES = LIHAT { $count } LOKASI
@@ -1077,16 +1077,16 @@ You-changed-filters-will-be-discarded = Anda telah mengubah penyaring, tetapi me
You-have-opted-out-of-the-Community-Taxon = Anda telah memilih untuk keluar dari Takson Komunitas
You-havent-joined-any-projects-yet = Anda belum bergabung dengan proyek apa pun!
You-havent-observed-any-species-yet = Anda belum mengamati spesies apa pun.
You-likely-observed-a-species-in-this-group = You likely observed a species in this group
You-likely-observed-this-species = You likely observed this species
You-may-have-observed-a-species-in-this-group = You may have observed a species in this group
You-may-have-observed-this-species = You may have observed this species
You-likely-observed-a-species-in-this-group = Kamu kemungkinan besar telah mengamati sebuah organisme dalam kelompok ini
You-likely-observed-this-species = Kamu kemungkinan besar telah mengamati spesies ini
You-may-have-observed-a-species-in-this-group = Kamu kemungkinan telah mengamati sebuah organisme dalam kelompok ini
You-may-have-observed-this-species = Kamu mungkin telah mengamati spesies ini
You-may-notice-changes-to-how-things-look-and-flow = Anda mungkin melihat perubahan pada tampilan dan alurnya. Anda dapat mengontrol pilihan Anda di pengaturan.
You-must-install-Google-Play-Services-to-sign-in-with-Google = Anda harus memasang Layanan Google Play untuk masuk dengan Google.
You-need-an-Internet-connection-to-do-that = Anda memerlukan koneksi Internet untuk melakukan hal tersebut.
You-need-log-in-to-do-that = Anda harus masuk untuk melakukannya.
You-observed-a-species-in-this-group = You observed a species in this group
You-observed-this-species = You observed this species
You-observed-a-species-in-this-group = Kamu telah mengamati sebuah spesies dalam kelompok ini
You-observed-this-species = Kamu telah mengamati spesies ini
You-will-see-notifications = Anda akan melihat notifikasi-notifikasi di sini setelah Anda masuk & mengunggah pengamatan.
Your-donation-to-iNaturalist = Sumbangan Anda untuk iNaturalist mendukung perkembangan dan stabilitas aplikasi seluler serta situs web yang menghubungkan jutaan orang dengan alam serta mendukung perlindungan keanekaragaman hayati di seluruh dunia!
Your-email-is-confirmed = Email Anda berhasil dikonfirmasi! Silakan masuk untuk melanjutkan prosesnya.

View File

@@ -51,7 +51,7 @@
"ALLOW-LOCATION-ACCESS": "IZINKAN AKSES LOKASI",
"Already-have-an-iNaturalist-account": "Sudah punya akun iNaturalist?",
"An-Internet-connection-is-required": "Koneksi Internet diperlukan untuk memuat lebih banyak pengamatan.",
"Analyzing-for-the-best-identification": "Analyzing for the best identification...",
"Analyzing-for-the-best-identification": "Menganalisis untuk identifikasi paling cocok...",
"Any--date": "Semua",
"Any--establishment-means": "Semua",
"Any--media-type": "Semua",
@@ -212,7 +212,7 @@
"Device-storage-full": "Penyimpanan perangkat penuh",
"Device-storage-full-description": "iNaturalist mungkin tidak bisa menyimpan gambar-gambar Anda atau dapat heng.",
"Disable-flash": "Matikan kilat",
"Disable-location": "Disable location",
"Disable-location": "Nonaktifkan lokasi",
"Disagreement": "*@{ $username } tidak setuju bahwa ini merupakan <0/>",
"DISCARD": "HAPUS",
"DISCARD-ALL": "HAPUS SEMUA",
@@ -245,7 +245,7 @@
"EMAIL": "EMAIL",
"EMAIL-DEBUG-LOGS": "EMAIL LOG DEBUG",
"Enable-flash": "Aktifkan kilat",
"Enable-location": "Enable location",
"Enable-location": "Aktifkan lokasi",
"Enable-notifications": "Aktifkan pemberitahuan",
"Endemic": "Endemik",
"Endemic-to-place": "Endemik di { $place }",
@@ -289,7 +289,7 @@
"Filter-by-uploaded-on-date": "Saring berdasarkan pengamatan yang diunggah pada tanggal tertentu",
"Filters": "Saringan",
"Flagged": "Telah Ditandai",
"Flash": "Flash",
"Flash": "Kilat",
"Flip-camera": "Balikkan kamera",
"FOLLOW": "IKUTI",
"FOLLOWING-X-PEOPLE": "{ $count ->\n [one] MENGIKUTI { $count } ORANG\n *[other] MENGIKUTI { $count } ORANG\n}",
@@ -303,8 +303,8 @@
"Google-Play-Services-Not-Installed": "Layanan Google Play Belum Terpasang",
"GRANT-PERMISSION": "BERI IZIN",
"Grant-Permission-title": "Berikan Izin",
"Group-Photos": "Group Photos",
"Group-photos-onboarding": "Group photos into observations make sure there is only one species per observation",
"Group-Photos": "Kelompokkan Gambar",
"Group-photos-onboarding": "Kelompokkan gambar menjadi pengamatan-pengamatannya pastikan hanya ada satu spesies per pengamatan",
"HELP": "BANTUAN",
"Help-create-Research-Grade-data-used-in-science-and-conservation": "Ikut membuat data Kelas Riset yang digunakan dalam sains dan konservasi.",
"Help-protect-species": "Bantu melindungi spesies",
@@ -332,7 +332,7 @@
"If-youre-seeing-this-error": "Jika Anda melihat pesan ini dan Anda sedang aktif, staf iNat telah diberitahu. Terima kasih telah menemukan bug! Jika Anda sedang tidak aktif, silakan mengambil tangkapan layar Anda dan kirimkan email kepada kami saat Anda sudah kembali aktif.",
"IGNORE-LOCATION": "ABAIKAN LOKASI",
"Ignore-notifications": "Abaikan pemberitahuan",
"Ignoring-location": "Ignoring location",
"Ignoring-location": "Mengabaikan lokasi",
"Import-Photos-From": "Impor Gambar Dari",
"IMPORT-X-OBSERVATIONS": "IMPOR { $count ->\n [one] 1 PENGAMATAN\n *[other] { $count } PENGAMATAN\n}",
"Improve-suggestions-by-using-your-location": "Tingkatkan kualitas saran dengan menggunakan lokasi Anda",
@@ -438,7 +438,7 @@
"Navigates-to-bulk-importer": "Pergi ke pengimpor massal",
"Navigates-to-camera": "Pergi ke kamera",
"Navigates-to-explore": "Pergi ke jelajah",
"Navigates-to-match-screen": "Navigates to match screen",
"Navigates-to-match-screen": "Pergi ke layar kecocokan",
"Navigates-to-notifications": "Pergi ke notifikasi",
"Navigates-to-observation-details": "Pergi ke layar informasi pengamatan",
"Navigates-to-observation-edit-screen": "Pergi ke layar edit pengamatan",
@@ -500,13 +500,13 @@
"Observations-View": "Tampilan Pengamatan",
"OBSERVATIONS-WITHOUT-NUMBER": "{ $count ->\n [one] PENGAMATAN\n *[other] PENGAMATAN\n}",
"OBSERVE-ORGANISMS": "AMATI ORGANISME",
"OBSERVED-AT--label": "OBSERVED AT",
"OBSERVED-AT--label": "DIAMATI PADA",
"OBSERVED-IN--label": "DIAMATI DI",
"Observers": "Pengamat",
"Observers-View": "Tampilan Pengamat",
"October": "Oktober",
"Offline-DQA-description": "AKD belum akurat. Periksa koneksi internet Anda dan coba lagi.",
"Offline-suggestions-may-differ-from-online": "Offline suggestions may differ from online suggestions, and taxon images and common names may not load.",
"Offline-suggestions-may-differ-from-online": "Rekomendasi luring bisa berbeda dengan rekomendasi daring. Gambar takson serta nama umumnya mungkin tidak termuat.",
"OK": "OK",
"Oldest-to-newest": "Terlama ke terbaru",
"Once-you-create-and-upload-observations": "Saat Anda membuat & mengunggah pengamatan, anggota lain dari komunitas kami dapat menambahkan identifikasi untuk membantu pengamatan Anda mencapai kelas riset.",
@@ -724,7 +724,7 @@
"Sign-in-with-Apple-Failed": "Gagal masuk dengan Apple",
"Sign-in-with-Google": "Masuk dengan Google",
"Sign-in-with-Google-Failed": "Gagal Masuk dengan Google",
"Skip-additional-suggestions": "Skip additional suggestions",
"Skip-additional-suggestions": "Lewati saran tambahan",
"Skip-for-now": "Lewati untuk sekarang",
"Something-went-wrong": "Terjadi kesalahan.",
"Sorry-this-observation-was-deleted": "Maaf, pengamatan ini sudah dihapus",
@@ -788,7 +788,7 @@
"This-is-a-wild-organism": "Ini adalah organisme liar dan tidak ditaruh di lokasi ini oleh manusia.",
"This-is-how-taxon-names-will-be-displayed": "Ini adalah pengaturan bagaimana semua nama takson akan ditampilkan kepada Anda di iNaturalist:",
"This-is-your-identification-other-people-may-help-confirm-it": "Ini adalah identifikasi Anda. Orang lain dapat membantu Anda mengonfirmasinya.",
"This-may-take-a-few-seconds": "This may take a few seconds.",
"This-may-take-a-few-seconds": "Proses ini dapat memerlukan beberapa detik.",
"This-observation-has-no-comments-or-identifications-yet": "Pengamatan ini belum memiliki komentar atau identifikasi.",
"This-observation-has-not-met-the-conditions-required-to-meet-Research-Grade": "Pengamatan ini belum memenuhi persyaratan yang dibutuhkan untuk memenuhi status Kelas Riset.",
"This-observation-is-not-eligible-for-research-grade-status": "Pengamatan ini belum memenuhi syarat untuk mendapatkan status kelas riset. Pelajari lebih lanjut di Penilaian Kualitas Data di bawah.",
@@ -797,9 +797,9 @@
"This-observation-needs-more-identifications-to-become-research-grade": "Pengamatan ini memerlukan lebih banyak identifikasi untuk mendapatkan kelas riset.",
"This-observer-has-opted-out-of-the-Community-Taxon": "Pengamat ini telah memilih untuk keluar dari Takson Komunitas",
"This-organism-was-placed-by-humans": "Organisme ini diperkenalkan ke tempat ini oleh manusia. Ini berlaku untuk makhluk-makhluk seperti tumbuhan di taman, hewan peliharaan, dan hewan-hewan di kebun binatang.",
"This-user-has-no-followers": "This user has no followers.",
"This-user-has-not-joined-any-projects": "This user has not joined any projects.",
"This-user-is-not-following-anyone": "This user is not following anyone.",
"This-user-has-no-followers": "Pengguna ini tidak memiliki pengikut.",
"This-user-has-not-joined-any-projects": "Pengguna ini belum bergabung dengan proyek apa pun.",
"This-user-is-not-following-anyone": "Pengguna ini tidak mengikuti siapa pun.",
"To-sync-your-observations-to-iNaturalist": "Untuk menyinkronkan pengamatan-pengamatan Anda ke iNaturalist, harap masuk terlebih dahulu.",
"To-view-nearby-organisms-please-enable-location": "Untuk melihat organisme-organisme di sekitar Anda, harap aktifkan lokasi.",
"To-view-nearby-projects-please-enable-location": "Untuk melihat proyek di sekitar Anda, harap aktifkan lokasi.",
@@ -836,7 +836,7 @@
"User": "Pengguna { $userHandle }",
"USERNAME-OR-EMAIL": "NAMA PENGGUNA ATAU EMAIL",
"Users": "Pengguna",
"Using-location": "Using location",
"Using-location": "Menggunakan lokasi",
"Verified-IDs-are-used-for-science-and-conservation": "ID terverifikasi digunakan untuk sains dan konservasi",
"Version-app-build": "Versi { $appVersion } ({ $buildVersion })",
"VIEW-ALL-X-PLACES": "LIHAT { $count } LOKASI",
@@ -914,16 +914,16 @@
"You-have-opted-out-of-the-Community-Taxon": "Anda telah memilih untuk keluar dari Takson Komunitas",
"You-havent-joined-any-projects-yet": "Anda belum bergabung dengan proyek apa pun!",
"You-havent-observed-any-species-yet": "Anda belum mengamati spesies apa pun.",
"You-likely-observed-a-species-in-this-group": "You likely observed a species in this group",
"You-likely-observed-this-species": "You likely observed this species",
"You-may-have-observed-a-species-in-this-group": "You may have observed a species in this group",
"You-may-have-observed-this-species": "You may have observed this species",
"You-likely-observed-a-species-in-this-group": "Kamu kemungkinan besar telah mengamati sebuah organisme dalam kelompok ini",
"You-likely-observed-this-species": "Kamu kemungkinan besar telah mengamati spesies ini",
"You-may-have-observed-a-species-in-this-group": "Kamu kemungkinan telah mengamati sebuah organisme dalam kelompok ini",
"You-may-have-observed-this-species": "Kamu mungkin telah mengamati spesies ini",
"You-may-notice-changes-to-how-things-look-and-flow": "Anda mungkin melihat perubahan pada tampilan dan alurnya. Anda dapat mengontrol pilihan Anda di pengaturan.",
"You-must-install-Google-Play-Services-to-sign-in-with-Google": "Anda harus memasang Layanan Google Play untuk masuk dengan Google.",
"You-need-an-Internet-connection-to-do-that": "Anda memerlukan koneksi Internet untuk melakukan hal tersebut.",
"You-need-log-in-to-do-that": "Anda harus masuk untuk melakukannya.",
"You-observed-a-species-in-this-group": "You observed a species in this group",
"You-observed-this-species": "You observed this species",
"You-observed-a-species-in-this-group": "Kamu telah mengamati sebuah spesies dalam kelompok ini",
"You-observed-this-species": "Kamu telah mengamati spesies ini",
"You-will-see-notifications": "Anda akan melihat notifikasi-notifikasi di sini setelah Anda masuk & mengunggah pengamatan.",
"Your-donation-to-iNaturalist": "Sumbangan Anda untuk iNaturalist mendukung perkembangan dan stabilitas aplikasi seluler serta situs web yang menghubungkan jutaan orang dengan alam serta mendukung perlindungan keanekaragaman hayati di seluruh dunia!",
"Your-email-is-confirmed": "Email Anda berhasil dikonfirmasi! Silakan masuk untuk melanjutkan prosesnya.",

View File

@@ -851,9 +851,9 @@ This-observation-needs-more-identifications = 此观察记录需要更多的鉴
This-observation-needs-more-identifications-to-become-research-grade = 此观察记录需要更多的鉴定才能成为研究等级。
This-observer-has-opted-out-of-the-Community-Taxon = 该观察者已选择退出社区分类单元
This-organism-was-placed-by-humans = 这种生物是被人类放置在这里的。这适用于花园植物、宠物和动物园动物。
This-user-has-no-followers = This user has no followers.
This-user-has-not-joined-any-projects = This user has not joined any projects.
This-user-is-not-following-anyone = This user is not following anyone.
This-user-has-no-followers = 此用户没有关注者。
This-user-has-not-joined-any-projects = 此用户没有加入任何项目。
This-user-is-not-following-anyone = 此用户没有关注任何人。
To-sync-your-observations-to-iNaturalist = 要将您的观察记录同步到 iNaturalist请登录。
To-view-nearby-organisms-please-enable-location = 要查看附近的生物,请启用位置。
To-view-nearby-projects-please-enable-location = 要查看附近的项目,请启用位置。

View File

@@ -797,9 +797,9 @@
"This-observation-needs-more-identifications-to-become-research-grade": "此观察记录需要更多的鉴定才能成为研究等级。",
"This-observer-has-opted-out-of-the-Community-Taxon": "该观察者已选择退出社区分类单元",
"This-organism-was-placed-by-humans": "这种生物是被人类放置在这里的。这适用于花园植物、宠物和动物园动物。",
"This-user-has-no-followers": "This user has no followers.",
"This-user-has-not-joined-any-projects": "This user has not joined any projects.",
"This-user-is-not-following-anyone": "This user is not following anyone.",
"This-user-has-no-followers": "此用户没有关注者。",
"This-user-has-not-joined-any-projects": "此用户没有加入任何项目。",
"This-user-is-not-following-anyone": "此用户没有关注任何人。",
"To-sync-your-observations-to-iNaturalist": "要将您的观察记录同步到 iNaturalist请登录。",
"To-view-nearby-organisms-please-enable-location": "要查看附近的生物,请启用位置。",
"To-view-nearby-projects-please-enable-location": "要查看附近的项目,请启用位置。",

View File

@@ -228,9 +228,6 @@ Closes-explanation = Closes explanation
# appear when you first install the app
Closes-introduction = Closes introduction
# Accessibility hint for button that closes the help that
# appears when you start a new observation for the first time
Closes-new-observation-explanation = Closes new observation explanation.
Closes-new-observation-options = Closes new observation options.
Closes-withdraw-id-sheet = Closes "Withdraw ID" sheet
# Heading for a section that describes people and organizations that
# collaborate with iNaturalist
@@ -274,7 +271,7 @@ Couldnt-create-comment = Couldn't create comment
Couldnt-create-identification-error = Couldn't create identification { $error }
Couldnt-create-identification-unknown-error = Couldn't create identification, unknown error.
CREATE-AN-ACCOUNT = CREATE AN ACCOUNT
Create-an-observation-evidence = Create an observation with no evidence
Create-observation-with-no-evidence = Create observation with no evidence
DATA-QUALITY = DATA QUALITY
DATA-QUALITY-ASSESSMENT = DATA QUALITY ASSESSMENT
# Label for button that navigates users to the data quality screen
@@ -563,6 +560,7 @@ Iconic-taxon-name = Iconic taxon name: { $iconicTaxon }
ID-Suggestions = ID Suggestions
# Short for: Identify with AI. Label for a button that will load identifications for a given photo/sound
ID-WITH-AI = ID WITH AI
ID-with-AI-Camera = ID with AI Camera
# Identification Status
ID-Withdrawn = ID Withdrawn
IDENTIFICATION = IDENTIFICATION
@@ -575,7 +573,6 @@ IDENTIFICATIONS-WITHOUT-NUMBER =
}
Identifiers = Identifiers
Identifiers-View = Identifiers View
Identify-an-organism = Identify an organism
# Title of screen asking for permission to access the camera
Identify-organisms-in-real-time-with-your-camera = Identify organisms in real time with your camera
# Onboarding slides
@@ -1210,8 +1207,8 @@ Switches-to-tab = Switches to { $tab } tab.
Sync-observations = Sync observations
Syncing = Syncing...
# Help text for the button that opens the multi-capture camera
Take-multiple-photos-of-a-single-organism = Take multiple photos of a single organism
Take-photo = Take photo
Take-photos = Take photos
# label in project requirements
Taxa = Taxa
TAXON = TAXON
@@ -1279,7 +1276,7 @@ Unreviewed-observations-only = Unreviewed observations only
Upload-Complete = Upload Complete
Upload-in-progress = Upload in progress
UPLOAD-NOW = UPLOAD NOW
Upload-photos-from-your-photo-library = Upload multiple photos from your photo library
Upload-photos = Upload photos
Upload-Progress = Upload { $uploadProgress } percent complete
UPLOAD-TO-INATURALIST = UPLOAD TO INATURALIST
# Shows the number of observations a user can upload to iNat from my observations page
@@ -1301,7 +1298,6 @@ Uploading-x-of-y-observations =
*[other] Uploading { $currentUploadCount } of { $total } observations
}
Use-iNaturalist-to-identify-any-living-thing = Use iNaturalist to identify any living thing
Use-iNaturalists-AI-Camera = Use iNaturalist's AI Camera to identify organisms in real time
# Text for a button prompting the user to grant access to location
USE-LOCATION = USE LOCATION
Use-the-devices-other-camera = Use the device's other camera.

View File

@@ -1,6 +1,6 @@
// @flow
import classNames from "classnames";
import AddObsButton from "components/AddObsModal/AddObsButton";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import { View } from "components/styledComponents";
import type { Node } from "react";
import React from "react";

View File

@@ -1,7 +1,4 @@
// @flow
import { INatIcon } from "components/SharedComponents";
import type { Node } from "react";
import React from "react";
import {
MD3LightTheme as DefaultTheme,
@@ -21,17 +18,14 @@ const theme = {
// keeping background here for react-native-paper TextInput
background: colors.white
}
};
} as const;
// eslint-disable-next-line react/jsx-props-no-spreading
const renderCustomIcon = props => <INatIcon {...props} />;
const renderCustomIcon = (
// eslint-disable-next-line react/jsx-props-no-spreading
( props: React.ComponentProps<typeof INatIcon> ) => <INatIcon {...props} />
);
type Props = {
// $FlowIgnore
children: unknown
}
const INatPaperProvider = ( { children }: Props ): Node => (
const INatPaperProvider = ( { children }: React.PropsWithChildren ) => (
<PaperProvider
settings={{
icon: renderCustomIcon

View File

@@ -92,6 +92,16 @@ interface RealmIdentification extends RealmObject {
user: RealmUser;
}
// https://github.com/inaturalist/iNaturalistAPI/blob/08e3aade068c50e02d0caf7a59c69ea87b70bc6e/lib/views/swagger_v1.yml.ejs#L2319-L2333
export type License =
| "cc-by"
| "cc-by-nc"
| "cc-by-nd"
| "cc-by-sa"
| "cc-by-nc-nd"
| "cc-by-nc-sa"
| "cc0"
export interface RealmObservationPojo {
_created_at?: Date;
_synced_at?: Date;
@@ -103,6 +113,7 @@ export interface RealmObservationPojo {
identifications: Array<RealmIdentification>;
identifications_viewed?: boolean;
latitude: number | null;
license_code: License | null;
longitude: number | null;
obscured?: boolean;
observationPhotos: Array<RealmObservationPhotoPojo>;
@@ -120,6 +131,7 @@ export interface RealmObservationPojo {
taxon_geoprivacy?: "open" | "private" | "obscured" | null;
time_observed_at?: string;
timeObservedAt?: string;
user: RealmUser;
uuid: string;
}

View File

@@ -1,7 +1,8 @@
// @flow
import { fetchUserMe } from "api/users";
import { RealmContext } from "providers/contexts";
import { useCallback, useEffect } from "react";
import { UpdateMode } from "realm";
import User from "realmModels/User";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import {
useAuthenticatedQuery,
@@ -10,7 +11,11 @@ import {
const { useRealm } = RealmContext;
const useUserMe = ( options: ?Object ): Object => {
interface UseUserMeOptions {
updateRealm?: boolean;
}
const useUserMe = ( options: UseUserMeOptions ) => {
const realm = useRealm( );
const currentUser = useCurrentUser( );
const updateRealm = options?.updateRealm;
@@ -32,7 +37,7 @@ const useUserMe = ( options: ?Object ): Object => {
const updateUser = useCallback( ( ) => {
if ( remoteUser && updateRealm ) {
safeRealmWrite( realm, ( ) => {
realm.create( "User", remoteUser, "modified" );
realm.create( User, remoteUser, UpdateMode.Modified );
}, "modifying current user via remote fetch in useUserMe" );
}
}, [

View File

@@ -1,5 +1,5 @@
import { screen, userEvent } from "@testing-library/react-native";
import AddObsButton from "components/AddObsModal/AddObsButton";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import i18next from "i18next";
import React from "react";
import { renderComponent } from "tests/helpers/render";
@@ -48,8 +48,8 @@ const longPress = async ( ) => {
};
const showNoEvidenceOption = ( ) => {
const noEvidenceButton = screen.getByLabelText(
i18next.t( "Observation-with-no-evidence" )
const noEvidenceButton = screen.getByTestId(
i18next.t( "observe-without-evidence-button" )
);
expect( noEvidenceButton ).toBeTruthy( );
return noEvidenceButton;
@@ -87,7 +87,7 @@ describe( "with advanced user layout", ( ) => {
} );
} );
it( "opens AddObsModal", async ( ) => {
it( "opens AddObsBottomSheet", async ( ) => {
renderComponent( <AddObsButton /> );
await regularPress( );
showNoEvidenceOption( );

View File

@@ -1,6 +1,5 @@
import { render, screen } from "@testing-library/react-native";
import AddObsModal from "components/AddObsModal/AddObsModal";
import i18next from "i18next";
import AddObsBottomSheet from "components/AddObsBottomSheet/AddObsBottomSheet";
import React from "react";
// Make sure the mock is using a recent-ish version
@@ -13,11 +12,11 @@ jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
}
} ) );
describe( "AddObsModal", ( ) => {
describe( "AddObsBottomSheet", ( ) => {
it( "shows the AI camera button", async ( ) => {
render( <AddObsModal closeModal={jest.fn( )} /> );
const aiCameraButton = screen.getByLabelText(
i18next.t( "AI-Camera" )
render( <AddObsBottomSheet closeModal={jest.fn( )} /> );
const aiCameraButton = screen.getByTestId(
"aicamera-button"
);
expect( aiCameraButton ).toBeOnTheScreen();
} );

View File

@@ -1,8 +1,8 @@
// Separate tests for iOS 9. AddObsModal sets some OS-specific constants at
// Separate tests for iOS 9. AddObsBottomSheet sets some OS-specific constants at
// load time that can't be altered at runtime, so we're using a separate test
// with a separate mock to control those load time values.
import { render, screen } from "@testing-library/react-native";
import AddObsModal from "components/AddObsModal/AddObsModal";
import AddObsBottomSheet from "components/AddObsBottomSheet/AddObsBottomSheet";
import i18next from "i18next";
import React from "react";
@@ -16,9 +16,9 @@ jest.mock( "react-native/Libraries/Utilities/Platform", () => ( {
}
} ) );
describe( "AddObsModal in iOS 9", ( ) => {
describe( "AddObsBottomSheet in iOS 9", ( ) => {
it( "hides AI camera button on older devices", async ( ) => {
render( <AddObsModal closeModal={jest.fn( )} /> );
render( <AddObsBottomSheet closeModal={jest.fn( )} /> );
const arCameraButton = screen.queryByLabelText(
i18next.t( "AI-Camera" )
);

View File

@@ -1,5 +1,6 @@
import { screen } from "@testing-library/react-native";
import AddObsButton from "components/AddObsModal/AddObsButton";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import { navigationRef } from "navigation/navigationUtils";
import React from "react";
import * as useCurrentUser from "sharedHooks/useCurrentUser";
import { zustandStorage } from "stores/useStore";
@@ -7,12 +8,10 @@ import factory from "tests/factory";
import { renderComponent } from "tests/helpers/render";
import setStoreStateLayout from "tests/helpers/setStoreStateLayout";
// Mock getCurrentRoute to return ObsList
jest.mock( "navigation/navigationUtils", () => ( {
getCurrentRoute: () => ( {
name: "ObsList"
} )
} ) );
// Mock methods needed to get the current route
navigationRef.isReady = jest.fn( () => true );
navigationRef.getCurrentRoute = jest.fn( () => ( { name: "ObsList" } ) );
navigationRef.addListener = jest.fn( () => jest.fn() );
const mockUser = factory( "LocalUser" );
@@ -34,7 +33,6 @@ describe( "AddObsButton", () => {
// Snapshot test
expect( screen ).toMatchSnapshot();
} );
it( "does not render tooltip in default state", () => {
renderComponent( <AddObsButton /> );
@@ -63,11 +61,10 @@ describe( "shows tooltip", () => {
renderComponent( <AddObsButton /> );
// Temporarily disabled the tooltip for new users, as it is freezing the app in some cases.
// const tooltipText = await screen.findByText(
// "Press and hold to view more options"
// );
// expect( tooltipText ).toBeTruthy();
const tooltipText = await screen.findByText(
"Press and hold to view more options"
);
expect( tooltipText ).toBeTruthy();
} );
it( "to new users only after they dismissed the account creation card", async () => {
@@ -89,11 +86,10 @@ describe( "shows tooltip", () => {
}
} );
// Temporarily disabled the tooltip for new users, as it is freezing the app in some cases.
// const tooltipTextAfter = await screen.findByText(
// "Press and hold to view more options"
// );
// expect( tooltipTextAfter ).toBeTruthy();
const tooltipTextAfter = await screen.findByText(
"Press and hold to view more options"
);
expect( tooltipTextAfter ).toBeTruthy();
} );
it( "to logged in users with more than 50 observations after card dismissal", async () => {
@@ -112,10 +108,9 @@ describe( "shows tooltip", () => {
}
} );
// Temporarily disabled the tooltip for new users, as it is freezing the app in some cases.
// const tooltipTextAfter = await screen.findByText(
// "Press and hold to view more options"
// );
// expect( tooltipTextAfter ).toBeTruthy();
const tooltipTextAfter = await screen.findByText(
"Press and hold to view more options"
);
expect( tooltipTextAfter ).toBeTruthy();
} );
} );