mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Merge branch 'main' into mob-512-implement-saved-match-screen
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" )
|
||||
|
||||
2
fastlane/metadata/android/en-US/changelogs/186.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/186.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
UPDATED
|
||||
* Minor dependency updates
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
142
package-lock.json
generated
@@ -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",
|
||||
|
||||
12
package.json
12
package.json
@@ -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",
|
||||
|
||||
169
src/components/AddObsBottomSheet/AddObsBottomSheet.tsx
Normal file
169
src/components/AddObsBottomSheet/AddObsBottomSheet.tsx
Normal 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;
|
||||
199
src/components/AddObsBottomSheet/AddObsButton.js
Normal file
199
src/components/AddObsBottomSheet/AddObsButton.js
Normal 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;
|
||||
54
src/components/AddObsBottomSheet/AddObsTooltip.tsx
Normal file
54
src/components/AddObsBottomSheet/AddObsTooltip.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 );
|
||||
@@ -1,4 +1,4 @@
|
||||
import AddObsButton from "components/AddObsModal/AddObsButton";
|
||||
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
|
||||
import {
|
||||
Body1,
|
||||
Body2,
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 )
|
||||
@@ -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;
|
||||
@@ -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]">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@ const Settings = ( ) => {
|
||||
setIsDefaultMode
|
||||
} = useLayoutPrefs( );
|
||||
|
||||
const handleValueChange = useCallback( newValue => {
|
||||
const handleValueChange = useCallback( ( newValue: boolean ) => {
|
||||
setIsDefaultMode( !newValue );
|
||||
}, [setIsDefaultMode] );
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 } )
|
||||
}
|
||||
|
||||
@@ -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 = ( {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 = 要查看附近的项目,请启用位置。
|
||||
|
||||
@@ -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": "要查看附近的项目,请启用位置。",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
12
src/realmModels/types.d.ts
vendored
12
src/realmModels/types.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" );
|
||||
}
|
||||
}, [
|
||||
@@ -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( );
|
||||
|
||||
@@ -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();
|
||||
} );
|
||||
@@ -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" )
|
||||
);
|
||||
@@ -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();
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user