mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Suggestions (using server data) (#821)
* UI overhaul for Suggestions * Updates to suggestions * Update permissions library and Podfile * Get tests passing * Add tests for suggestions * Update snapshot * Add comment prompt & box to TaxonSearch * Add empty state * Nav and loading fixes * Add more tests to Suggestions flow * Fix tests * Fix tests
This commit is contained in:
committed by
GitHub
parent
f29b2b566c
commit
7d20f6aa81
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
"data": [
|
||||
{
|
||||
"path": "assets/fonts/INatIcon.ttf",
|
||||
"sha1": "62da083366bb9e2e214e271c04fc5823a467b396"
|
||||
"sha1": "d3d24b237a7ef666ef81aff93cde57e534a676a3"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney-BookItalic-Pro.otf",
|
||||
|
||||
Binary file not shown.
37
ios/Podfile
37
ios/Podfile
@@ -1,11 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# setup instructions from https://www.npmjs.com/package/react-native-permissions
|
||||
def node_require(script)
|
||||
# Resolve script with node to allow for hoisting
|
||||
require Pod::Executable.execute_command('node', ['-p',
|
||||
"require.resolve(
|
||||
'#{script}',
|
||||
{paths: [process.argv[1]]},
|
||||
)", __dir__]).strip
|
||||
end
|
||||
|
||||
node_require('react-native/scripts/react_native_pods.rb')
|
||||
node_require('react-native-permissions/scripts/setup.rb')
|
||||
|
||||
require_relative "../node_modules/react-native/scripts/react_native_pods"
|
||||
require_relative "../node_modules/@react-native-community/cli-platform-ios/native_modules"
|
||||
require_relative '../node_modules/react-native-permissions/scripts/setup'
|
||||
|
||||
platform :ios, min_ios_version_supported
|
||||
prepare_react_native_project!
|
||||
|
||||
# ⬇️ uncomment wanted permissions
|
||||
setup_permissions([
|
||||
# 'AppTrackingTransparency',
|
||||
# 'BluetoothPeripheral',
|
||||
# 'Calendars',
|
||||
'Camera',
|
||||
# 'Contacts',
|
||||
# 'FaceID',
|
||||
'LocationAccuracy',
|
||||
'LocationAlways',
|
||||
'LocationWhenInUse',
|
||||
'MediaLibrary',
|
||||
'Microphone',
|
||||
# 'Motion',
|
||||
# 'Notifications',
|
||||
'PhotoLibrary',
|
||||
'PhotoLibraryAddOnly',
|
||||
# 'Reminders',
|
||||
# 'Siri',
|
||||
# 'SpeechRecognition',
|
||||
# 'StoreKit',
|
||||
])
|
||||
|
||||
# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
|
||||
# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded
|
||||
#
|
||||
|
||||
@@ -418,7 +418,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNLocalize (2.2.6):
|
||||
- React-Core
|
||||
- RNPermissions (3.8.0):
|
||||
- RNPermissions (3.10.0):
|
||||
- React-Core
|
||||
- RNReanimated (2.17.0):
|
||||
- DoubleConversion
|
||||
@@ -447,7 +447,7 @@ PODS:
|
||||
- React-RCTText
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNScreens (3.20.0):
|
||||
- RNScreens (3.21.1):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- RNShareMenu (6.0.0):
|
||||
@@ -764,9 +764,9 @@ SPEC CHECKSUMS:
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNGestureHandler: 6e4dc6b7ab3a385386d4e36228bd065e5a611394
|
||||
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
|
||||
RNPermissions: 46f97a9fb7a4ce9a3fbf3a9dde0e6f83eb7bd170
|
||||
RNPermissions: 0332875c444efe864dd97071dc848529bd7cc692
|
||||
RNReanimated: f186e85d9f28c9383d05ca39e11dd194f59093ec
|
||||
RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f
|
||||
RNScreens: d3675ab2878704de70c9dae57fa5d024802404cc
|
||||
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
|
||||
RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
@@ -774,6 +774,6 @@ SPEC CHECKSUMS:
|
||||
VisionCameraPluginInatVision: 79bb258db75218889c74d0897ecba676492c4def
|
||||
Yoga: 39310a10944fc864a7550700de349183450f8aaa
|
||||
|
||||
PODFILE CHECKSUM: 12011c0d1d148a45ad35fd9c47bc095139411027
|
||||
PODFILE CHECKSUM: 77ed9526d4011b245ce5afa1ea331dea4c67d753
|
||||
|
||||
COCOAPODS: 1.13.0
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
8B65ED3129F575C10054CCEF /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B65ED2F29F575C10054CCEF /* MainInterface.storyboard */; };
|
||||
8B65ED3529F575C10054CCEF /* iNaturalistReactNative-ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8B65ED2B29F575C10054CCEF /* iNaturalistReactNative-ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
8B65ED3B29F575FE0054CCEF /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B65ED3A29F575FE0054CCEF /* ShareViewController.swift */; };
|
||||
8C27575C16AD4AB88AFBEAD7 /* INatIcon.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 046B62AFE69547678922E815 /* INatIcon.ttf */; };
|
||||
A019DB3A4661689827F5BB56 /* libPods-iNaturalistReactNative.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 486ED9661FEC89EDDBE3DA02 /* libPods-iNaturalistReactNative.a */; };
|
||||
A252B2AEA64E47C9AC1D20E8 /* Whitney-Light-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA9D41ECEBFA4C38B74009B3 /* Whitney-Light-Pro.otf */; };
|
||||
B7727B0D0C414FFF99814FDF /* INatIcon.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 68976D05824745ECA696C47C /* INatIcon.ttf */; };
|
||||
BA2479FA3D7B40A7BEF7B3CD /* Whitney-Medium-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = D09FA3A0162844FF80A5EF96 /* Whitney-Medium-Pro.otf */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
00E356EE1AD99517003FC87E /* iNaturalistReactNativeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iNaturalistReactNativeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
00E356F21AD99517003FC87E /* iNaturalistReactNativeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iNaturalistReactNativeTests.m; sourceTree = "<group>"; };
|
||||
046B62AFE69547678922E815 /* INatIcon.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = INatIcon.ttf; path = ../assets/fonts/INatIcon.ttf; sourceTree = "<group>"; };
|
||||
13B07F961A680F5B00A75B9A /* iNaturalistReactNative.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iNaturalistReactNative.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = iNaturalistReactNative/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = iNaturalistReactNative/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
@@ -77,6 +76,7 @@
|
||||
374CB22E29943E63005885ED /* Whitney-BookItalic-Pro.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Whitney-BookItalic-Pro.otf"; path = "../assets/fonts/Whitney-BookItalic-Pro.otf"; sourceTree = "<group>"; };
|
||||
486ED9661FEC89EDDBE3DA02 /* libPods-iNaturalistReactNative.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iNaturalistReactNative.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6836402D214AF8075A93F130 /* Pods-iNaturalistReactNative.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative.release.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative/Pods-iNaturalistReactNative.release.xcconfig"; sourceTree = "<group>"; };
|
||||
68976D05824745ECA696C47C /* INatIcon.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = INatIcon.ttf; path = ../assets/fonts/INatIcon.ttf; sourceTree = "<group>"; };
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = iNaturalistReactNative/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
8B65ED2B29F575C10054CCEF /* iNaturalistReactNative-ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "iNaturalistReactNative-ShareExtension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8B65ED3029F575C10054CCEF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
@@ -232,7 +232,7 @@
|
||||
D09FA3A0162844FF80A5EF96 /* Whitney-Medium-Pro.otf */,
|
||||
374CB22E29943E63005885ED /* Whitney-BookItalic-Pro.otf */,
|
||||
EE004FD2EC174086A7AB2908 /* inaturalisticons.ttf */,
|
||||
046B62AFE69547678922E815 /* INatIcon.ttf */,
|
||||
68976D05824745ECA696C47C /* INatIcon.ttf */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -363,7 +363,7 @@
|
||||
A252B2AEA64E47C9AC1D20E8 /* Whitney-Light-Pro.otf in Resources */,
|
||||
BA2479FA3D7B40A7BEF7B3CD /* Whitney-Medium-Pro.otf in Resources */,
|
||||
4FB3B444D46A4115B867B9CC /* inaturalisticons.ttf in Resources */,
|
||||
8C27575C16AD4AB88AFBEAD7 /* INatIcon.ttf in Resources */,
|
||||
B7727B0D0C414FFF99814FDF /* INatIcon.ttf in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"data": [
|
||||
{
|
||||
"path": "assets/fonts/INatIcon.ttf",
|
||||
"sha1": "62da083366bb9e2e214e271c04fc5823a467b396"
|
||||
"sha1": "d3d24b237a7ef666ef81aff93cde57e534a676a3"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney-BookItalic-Pro.otf",
|
||||
|
||||
161
package-lock.json
generated
161
package-lock.json
generated
@@ -82,13 +82,13 @@
|
||||
"react-native-open-maps": "^0.4.3",
|
||||
"react-native-orientation-locker": "github:wonday/react-native-orientation-locker",
|
||||
"react-native-paper": "^5.10.5",
|
||||
"react-native-permissions": "3.8.0",
|
||||
"react-native-permissions": "^3.10.0",
|
||||
"react-native-picker-select": "8.0.4",
|
||||
"react-native-reanimated": "^2.17.0",
|
||||
"react-native-reanimated-carousel": "^3.4.0",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "^4.7.2",
|
||||
"react-native-screens": "3.20.0",
|
||||
"react-native-screens": "^3.21.1",
|
||||
"react-native-sensitive-info": "^6.0.0-alpha.9",
|
||||
"react-native-share-menu": "^6.0.0",
|
||||
"react-native-svg": "13.9.0",
|
||||
@@ -23448,13 +23448,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.8.0.tgz",
|
||||
"integrity": "sha512-BfZ7ksgdpGchHZH8M/kxCGZbWeACANbnPmb3hNjVOMDQusc4PWlPpobX3eBqYMSKbpi7bMECeV9BVU4QuwAf9A==",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
"pkg-dir": "^5.0.0"
|
||||
},
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.10.0.tgz",
|
||||
"integrity": "sha512-6SB6JInfC0u54Wco8M1QsRoOGThnVjrhaks5IDWyYgwfi6JpXfTIi+F9fZZmRycyyPCgU8vGtht/5gF4VWEB/A==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13.1",
|
||||
"react-native": ">=0.63.3",
|
||||
@@ -23466,82 +23462,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions/node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"dependencies": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions/node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"dependencies": {
|
||||
"p-locate": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions/node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions/node_modules/p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions/node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-permissions/node_modules/pkg-dir": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
|
||||
"integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
|
||||
"dependencies": {
|
||||
"find-up": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-picker-select": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-8.0.4.tgz",
|
||||
@@ -23626,9 +23546,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-screens": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.20.0.tgz",
|
||||
"integrity": "sha512-joWUKWAVHxymP3mL9gYApFHAsbd9L6ZcmpoZa6Sl3W/82bvvNVMqcfP7MeNqVCg73qZ8yL4fW+J/syusHleUgg==",
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.21.1.tgz",
|
||||
"integrity": "sha512-asyqTA/0Ij9Sa16zB9YCUmZku5bf8RVKS0FKKhS2jfmF98BFt5XV6ldvM+Ze9cwgssSyRz4jYOIDSK1Wr5ZaYQ==",
|
||||
"dependencies": {
|
||||
"react-freeze": "^1.0.0",
|
||||
"warn-once": "^0.1.0"
|
||||
@@ -44801,61 +44721,10 @@
|
||||
}
|
||||
},
|
||||
"react-native-permissions": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.8.0.tgz",
|
||||
"integrity": "sha512-BfZ7ksgdpGchHZH8M/kxCGZbWeACANbnPmb3hNjVOMDQusc4PWlPpobX3eBqYMSKbpi7bMECeV9BVU4QuwAf9A==",
|
||||
"requires": {
|
||||
"picocolors": "^1.0.0",
|
||||
"pkg-dir": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"requires": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"requires": {
|
||||
"p-locate": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"requires": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"requires": {
|
||||
"p-limit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
|
||||
"integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
|
||||
"requires": {
|
||||
"find-up": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.10.0.tgz",
|
||||
"integrity": "sha512-6SB6JInfC0u54Wco8M1QsRoOGThnVjrhaks5IDWyYgwfi6JpXfTIi+F9fZZmRycyyPCgU8vGtht/5gF4VWEB/A==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-picker-select": {
|
||||
"version": "8.0.4",
|
||||
@@ -44918,9 +44787,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-screens": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.20.0.tgz",
|
||||
"integrity": "sha512-joWUKWAVHxymP3mL9gYApFHAsbd9L6ZcmpoZa6Sl3W/82bvvNVMqcfP7MeNqVCg73qZ8yL4fW+J/syusHleUgg==",
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.21.1.tgz",
|
||||
"integrity": "sha512-asyqTA/0Ij9Sa16zB9YCUmZku5bf8RVKS0FKKhS2jfmF98BFt5XV6ldvM+Ze9cwgssSyRz4jYOIDSK1Wr5ZaYQ==",
|
||||
"requires": {
|
||||
"react-freeze": "^1.0.0",
|
||||
"warn-once": "^0.1.0"
|
||||
|
||||
@@ -103,13 +103,13 @@
|
||||
"react-native-open-maps": "^0.4.3",
|
||||
"react-native-orientation-locker": "github:wonday/react-native-orientation-locker",
|
||||
"react-native-paper": "^5.10.5",
|
||||
"react-native-permissions": "3.8.0",
|
||||
"react-native-permissions": "^3.10.0",
|
||||
"react-native-picker-select": "8.0.4",
|
||||
"react-native-reanimated": "^2.17.0",
|
||||
"react-native-reanimated-carousel": "^3.4.0",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "^4.7.2",
|
||||
"react-native-screens": "3.20.0",
|
||||
"react-native-screens": "^3.21.1",
|
||||
"react-native-sensitive-info": "^6.0.0-alpha.9",
|
||||
"react-native-share-menu": "^6.0.0",
|
||||
"react-native-svg": "13.9.0",
|
||||
|
||||
26
src/api/computerVision.js
Normal file
26
src/api/computerVision.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import Taxon from "realmModels/Taxon";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const PARAMS = {
|
||||
fields: {
|
||||
taxon: Taxon.TAXON_FIELDS
|
||||
}
|
||||
};
|
||||
|
||||
const scoreImage = async (
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.computervision.score_image( { ...PARAMS, ...params }, opts );
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export default scoreImage;
|
||||
@@ -1,88 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {
|
||||
Body3, INatIcon, INatIconButton,
|
||||
TextInputSheet, ViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator
|
||||
} from "react-native-paper";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
import TaxonSearch from "./TaxonSearch";
|
||||
|
||||
type Props = {
|
||||
setComment: Function,
|
||||
comment: string,
|
||||
taxonQuery: string,
|
||||
setTaxonQuery: Function,
|
||||
createId: Function,
|
||||
loading: boolean
|
||||
};
|
||||
|
||||
const AddID = ( {
|
||||
setComment, comment, taxonQuery, setTaxonQuery, createId, loading
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const [showAddCommentSheet, setShowAddCommentSheet] = useState( false );
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
useEffect( ( ) => {
|
||||
const addCommentIcon = ( ) => (
|
||||
<INatIconButton
|
||||
icon="add-comment-outline"
|
||||
onPress={( ) => setShowAddCommentSheet( true )}
|
||||
accessibilityLabel={t( "Add-comment" )}
|
||||
size={25}
|
||||
/>
|
||||
);
|
||||
|
||||
navigation.setOptions( {
|
||||
headerRight: addCommentIcon
|
||||
} );
|
||||
}, [navigation, t] );
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
{showAddCommentSheet && (
|
||||
<TextInputSheet
|
||||
handleClose={( ) => setShowAddCommentSheet( false )}
|
||||
headerText={t( "ADD-OPTIONAL-COMMENT" )}
|
||||
snapPoints={[416]}
|
||||
confirm={textInput => setComment( textInput )}
|
||||
/>
|
||||
)}
|
||||
<View>
|
||||
{ comment && comment.length > 0 && (
|
||||
<View className="bg-lightGray mx-6 p-5 rounded-lg flex-row items-center">
|
||||
<INatIcon
|
||||
name="comments-outline"
|
||||
size={22}
|
||||
/>
|
||||
<Body3 className="ml-4 shrink">{ comment }</Body3>
|
||||
</View>
|
||||
) }
|
||||
{loading && (
|
||||
<View
|
||||
className="absolute self-center z-10 pt-[30px]"
|
||||
testID="AddID.ActivityIndicator"
|
||||
>
|
||||
<ActivityIndicator large />
|
||||
</View>
|
||||
)}
|
||||
<TaxonSearch
|
||||
taxonQuery={taxonQuery}
|
||||
setTaxonQuery={setTaxonQuery}
|
||||
onTaxonChosen={createId}
|
||||
/>
|
||||
</View>
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddID;
|
||||
@@ -1,136 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { createIdentification } from "api/identifications";
|
||||
import { ObsEditContext, RealmContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import uuid from "react-native-uuid";
|
||||
import {
|
||||
useAuthenticatedMutation,
|
||||
useCurrentUser,
|
||||
useLocalObservation,
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
|
||||
import AddID from "./AddID";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
type Props = {
|
||||
route: {
|
||||
params: {
|
||||
observationUUID?: string,
|
||||
createRemoteIdentification?: boolean,
|
||||
belongsToCurrentUser?: boolean
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const AddIDContainer = ( { route }: Props ): Node => {
|
||||
const [comment, setComment] = useState( "" );
|
||||
const [loading, setLoading] = useState( false );
|
||||
const [taxonQuery, setTaxonQuery] = useState( "" );
|
||||
const {
|
||||
updateObservationKeys
|
||||
} = useContext( ObsEditContext );
|
||||
const currentUser = useCurrentUser( );
|
||||
const realm = useRealm( );
|
||||
const observationUUID = route?.params?.observationUUID;
|
||||
const createRemoteIdentification = route?.params?.createRemoteIdentification;
|
||||
const belongsToCurrentUser = route?.params?.belongsToCurrentUser;
|
||||
const localObservation = useLocalObservation( observationUUID );
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const navigation = useNavigation( );
|
||||
|
||||
const showErrorAlert = error => Alert.alert( "Error", error, [{ text: t( "OK" ) }], {
|
||||
cancelable: true
|
||||
} );
|
||||
|
||||
const createIdentificationMutation = useAuthenticatedMutation(
|
||||
( idParams, optsWithAuth ) => createIdentification( idParams, optsWithAuth ),
|
||||
{
|
||||
onSuccess: data => {
|
||||
if ( belongsToCurrentUser ) {
|
||||
realm?.write( ( ) => {
|
||||
const localIdentifications = localObservation?.identifications;
|
||||
const newIdentification = data[0];
|
||||
newIdentification.user = currentUser;
|
||||
newIdentification.taxon = realm?.objectForPrimaryKey(
|
||||
"Taxon",
|
||||
newIdentification.taxon.id
|
||||
) || newIdentification.taxon;
|
||||
const realmIdentification = realm?.create( "Identification", newIdentification );
|
||||
localIdentifications.push( realmIdentification );
|
||||
} );
|
||||
}
|
||||
// navigate back to ObsDetails
|
||||
navigation.goBack( );
|
||||
},
|
||||
onError: e => {
|
||||
let error = null;
|
||||
if ( e ) {
|
||||
error = t( "Couldnt-create-identification-error", { error: e.message } );
|
||||
} else {
|
||||
error = t( "Couldnt-create-identification-unknown-error" );
|
||||
}
|
||||
showErrorAlert( error );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const formatIdentification = taxon => {
|
||||
const newIdent = {
|
||||
uuid: uuid.v4(),
|
||||
body: comment,
|
||||
taxon
|
||||
};
|
||||
|
||||
return newIdent;
|
||||
};
|
||||
|
||||
const createId = identification => {
|
||||
setLoading( true );
|
||||
const newIdentification = formatIdentification( identification );
|
||||
if ( createRemoteIdentification ) {
|
||||
createIdentificationMutation.mutate( {
|
||||
identification: {
|
||||
observation_id: observationUUID,
|
||||
taxon_id: newIdentification.taxon.id,
|
||||
body: newIdentification.body
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
updateObservationKeys( {
|
||||
taxon: newIdentification.taxon
|
||||
} );
|
||||
navigation.goBack( );
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(
|
||||
( ) => {
|
||||
navigation.addListener( "blur", ( ) => {
|
||||
setTaxonQuery( "" );
|
||||
setComment( "" );
|
||||
setLoading( false );
|
||||
} );
|
||||
},
|
||||
[navigation]
|
||||
);
|
||||
|
||||
return (
|
||||
<AddID
|
||||
setComment={setComment}
|
||||
comment={comment}
|
||||
taxonQuery={taxonQuery}
|
||||
setTaxonQuery={setTaxonQuery}
|
||||
createId={createId}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddIDContainer;
|
||||
@@ -1,76 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import fetchSearchResults from "api/search";
|
||||
import {
|
||||
Body2,
|
||||
SearchBar,
|
||||
TaxonResult
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import Taxon from "realmModels/Taxon";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
|
||||
type Props = {
|
||||
taxonQuery: string,
|
||||
setTaxonQuery: Function,
|
||||
onTaxonChosen: Function
|
||||
};
|
||||
|
||||
const TaxonSearch = ( {
|
||||
taxonQuery,
|
||||
setTaxonQuery,
|
||||
onTaxonChosen
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const { data: taxonList } = useAuthenticatedQuery(
|
||||
["fetchSearchResults", taxonQuery],
|
||||
optsWithAuth => fetchSearchResults(
|
||||
{
|
||||
q: taxonQuery,
|
||||
sources: "taxa",
|
||||
fields: {
|
||||
taxon: Taxon.TAXON_FIELDS
|
||||
}
|
||||
},
|
||||
optsWithAuth
|
||||
)
|
||||
);
|
||||
|
||||
const renderEmptyComponent = ( ) => (
|
||||
<Body2 className="self-center">
|
||||
{t( "Search-for-a-taxon-to-add-an-identification" )}
|
||||
</Body2>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="mx-6">
|
||||
<SearchBar
|
||||
handleTextChange={setTaxonQuery}
|
||||
value={taxonQuery}
|
||||
testID="SearchTaxon"
|
||||
containerClass="pb-5 mt-3"
|
||||
/>
|
||||
</View>
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
data={taxonList}
|
||||
renderItem={( { item } ) => (
|
||||
<TaxonResult
|
||||
taxon={item}
|
||||
handleCheckmarkPress={( ) => onTaxonChosen( item )}
|
||||
testID={`Search.taxa.${item.id}`}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => item.id}
|
||||
ListEmptyComponent={renderEmptyComponent}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaxonSearch;
|
||||
@@ -29,7 +29,7 @@ type Props = {
|
||||
rotation?: {
|
||||
value: number
|
||||
},
|
||||
setMediaViewerUris: Function,
|
||||
setPhotoEvidenceUris: Function,
|
||||
photoUris: Array<string>,
|
||||
setSelectedPhotoIndex: Function
|
||||
}
|
||||
@@ -60,7 +60,7 @@ const PhotoCarousel = ( {
|
||||
isLargeScreen,
|
||||
isTablet,
|
||||
rotation,
|
||||
setMediaViewerUris,
|
||||
setPhotoEvidenceUris,
|
||||
photoUris,
|
||||
setSelectedPhotoIndex
|
||||
}: Props ): Node => {
|
||||
@@ -136,7 +136,7 @@ const PhotoCarousel = ( {
|
||||
deletePhoto( item );
|
||||
} else {
|
||||
setSelectedPhotoIndex( index );
|
||||
setMediaViewerUris( [...photoUris] );
|
||||
setPhotoEvidenceUris( [...photoUris] );
|
||||
navigation.navigate( "MediaViewer" );
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -33,7 +33,7 @@ const PhotoPreview = ( {
|
||||
const {
|
||||
cameraPreviewUris: photoUris,
|
||||
deletePhotoFromObservation,
|
||||
setMediaViewerUris,
|
||||
setPhotoEvidenceUris,
|
||||
setSelectedPhotoIndex
|
||||
} = useContext( ObsEditContext );
|
||||
const wrapperDim = isLargeScreen
|
||||
@@ -99,7 +99,7 @@ const PhotoPreview = ( {
|
||||
deletePhoto={deletePhoto}
|
||||
photoUris={photoUris}
|
||||
rotation={rotation}
|
||||
setMediaViewerUris={setMediaViewerUris}
|
||||
setPhotoEvidenceUris={setPhotoEvidenceUris}
|
||||
takingPhoto={takingPhoto}
|
||||
isLargeScreen={isLargeScreen}
|
||||
isTablet={isTablet}
|
||||
|
||||
@@ -25,13 +25,13 @@ const MediaViewer = ( ): Node => {
|
||||
const [warningSheet, setWarningSheet] = useState( false );
|
||||
const {
|
||||
deletePhotoFromObservation,
|
||||
mediaViewerUris,
|
||||
photoEvidenceUris,
|
||||
selectedPhotoIndex,
|
||||
setSelectedPhotoIndex
|
||||
} = useContext( ObsEditContext );
|
||||
|
||||
const atFirstPhoto = selectedPhotoIndex === 0;
|
||||
const atLastPhoto = selectedPhotoIndex === mediaViewerUris.length - 1;
|
||||
const atLastPhoto = selectedPhotoIndex === photoEvidenceUris.length - 1;
|
||||
|
||||
const handleScrollLeft = index => {
|
||||
if ( atFirstPhoto ) { return; }
|
||||
@@ -48,15 +48,15 @@ const MediaViewer = ( ): Node => {
|
||||
const { isLandscapeMode, screenWidth } = useDeviceOrientation( );
|
||||
const isLargeScreen = screenWidth > BREAKPOINTS.md;
|
||||
|
||||
const numOfPhotos = mediaViewerUris.length;
|
||||
const numOfPhotos = photoEvidenceUris.length;
|
||||
|
||||
const showWarningSheet = ( ) => setWarningSheet( true );
|
||||
const hideWarningSheet = ( ) => setWarningSheet( false );
|
||||
|
||||
const deletePhoto = ( ) => {
|
||||
deletePhotoFromObservation( mediaViewerUris[selectedPhotoIndex] );
|
||||
deletePhotoFromObservation( photoEvidenceUris[selectedPhotoIndex] );
|
||||
hideWarningSheet( );
|
||||
if ( mediaViewerUris.length === 0 ) {
|
||||
if ( photoEvidenceUris.length === 0 ) {
|
||||
navigation.goBack( );
|
||||
} else if ( selectedPhotoIndex !== 0 ) {
|
||||
setSelectedPhotoIndex( selectedPhotoIndex - 1 );
|
||||
@@ -115,13 +115,13 @@ const MediaViewer = ( ): Node => {
|
||||
/>
|
||||
)}
|
||||
<MainPhotoDisplay
|
||||
photoUris={mediaViewerUris}
|
||||
photoUris={photoEvidenceUris}
|
||||
selectedPhotoIndex={selectedPhotoIndex}
|
||||
handleScrollEndDrag={handleScrollEndDrag}
|
||||
horizontalScroll={horizontalScroll}
|
||||
/>
|
||||
<PhotoSelector
|
||||
photoUris={mediaViewerUris}
|
||||
photoUris={photoEvidenceUris}
|
||||
scrollToIndex={scrollToIndex}
|
||||
isLargeScreen={isLargeScreen}
|
||||
isLandscapeMode={isLandscapeMode}
|
||||
|
||||
@@ -12,13 +12,13 @@ import { getShadowStyle } from "styles/global";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
type Props = {
|
||||
navToAddID: Function,
|
||||
navToSuggestions: Function,
|
||||
showCommentBox: Function,
|
||||
openCommentBox: Function
|
||||
}
|
||||
|
||||
const FloatingButtons = ( {
|
||||
navToAddID,
|
||||
navToSuggestions,
|
||||
openCommentBox,
|
||||
showCommentBox
|
||||
}: Props ): Node => {
|
||||
@@ -47,7 +47,7 @@ const FloatingButtons = ( {
|
||||
/>
|
||||
<Button
|
||||
text={t( "SUGGEST-ID" )}
|
||||
onPress={navToAddID}
|
||||
onPress={navToSuggestions}
|
||||
className="w-1/2 mx-6"
|
||||
testID="ObsDetail.cvSuggestionsButton"
|
||||
accessibilityRole="link"
|
||||
|
||||
@@ -20,7 +20,7 @@ import DetailsTab from "./DetailsTab/DetailsTab";
|
||||
import Header from "./Header";
|
||||
|
||||
type Props = {
|
||||
navToAddID: Function,
|
||||
navToSuggestions: Function,
|
||||
onCommentAdded: Function,
|
||||
openCommentBox: Function,
|
||||
tabs: Array<Object>,
|
||||
@@ -40,7 +40,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const ObsDetails = ( {
|
||||
navToAddID,
|
||||
navToSuggestions,
|
||||
onCommentAdded,
|
||||
openCommentBox,
|
||||
tabs,
|
||||
@@ -97,7 +97,7 @@ const ObsDetails = ( {
|
||||
</ScrollView>
|
||||
{showActivityTab && (
|
||||
<FloatingButtons
|
||||
navToAddID={navToAddID}
|
||||
navToSuggestions={navToSuggestions}
|
||||
openCommentBox={openCommentBox}
|
||||
showCommentBox={showCommentBox}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useFocusEffect, useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { createComment } from "api/comments";
|
||||
import { createIdentification } from "api/identifications";
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
fetchRemoteObservation,
|
||||
markObservationUpdatesViewed
|
||||
} from "api/observations";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import { ObsEditContext, RealmContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useEffect, useReducer
|
||||
useCallback,
|
||||
useContext, useEffect, useReducer
|
||||
} from "react";
|
||||
import { Alert, LogBox } from "react-native";
|
||||
import Observation from "realmModels/Observation";
|
||||
@@ -98,6 +99,10 @@ const reducer = ( state, action ) => {
|
||||
};
|
||||
|
||||
const ObsDetailsContainer = ( ): Node => {
|
||||
const {
|
||||
setObservations,
|
||||
setLastScreen
|
||||
} = useContext( ObsEditContext );
|
||||
const currentUser = useCurrentUser( );
|
||||
const { params } = useRoute();
|
||||
const { uuid } = params;
|
||||
@@ -138,6 +143,14 @@ const ObsDetailsContainer = ( ): Node => {
|
||||
|
||||
const belongsToCurrentUser = observation?.user?.login === currentUser?.login;
|
||||
|
||||
useFocusEffect(
|
||||
// this ensures activity items load after a user taps suggest id
|
||||
// and adds a remote id on the Suggestions screen
|
||||
useCallback( ( ) => {
|
||||
queryClient.invalidateQueries( "fetchRemoteObservation" );
|
||||
}, [queryClient] )
|
||||
);
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( !observationShown ) {
|
||||
dispatch( {
|
||||
@@ -290,12 +303,10 @@ const ObsDetailsContainer = ( ): Node => {
|
||||
}
|
||||
}, [localObservation, markViewedMutation, uuid] );
|
||||
|
||||
const navToAddID = ( ) => {
|
||||
navigation.navigate( "AddID", {
|
||||
observationUUID: uuid,
|
||||
createRemoteIdentification: true,
|
||||
belongsToCurrentUser
|
||||
} );
|
||||
const navToSuggestions = ( ) => {
|
||||
setObservations( [observation] );
|
||||
setLastScreen( "ObsDetails" );
|
||||
navigation.navigate( "Suggestions" );
|
||||
};
|
||||
|
||||
const showActivityTab = currentTabId === ACTIVITY_TAB_ID;
|
||||
@@ -332,7 +343,7 @@ const ObsDetailsContainer = ( ): Node => {
|
||||
|
||||
return (
|
||||
<ObsDetails
|
||||
navToAddID={navToAddID}
|
||||
navToSuggestions={navToSuggestions}
|
||||
onCommentAdded={onCommentAdded}
|
||||
openCommentBox={openCommentBox}
|
||||
tabs={tabs}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import classnames from "classnames";
|
||||
import { INatIcon } from "components/SharedComponents";
|
||||
import { Image, Pressable, View } from "components/styledComponents";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import DraggableFlatList, { ScaleDecorator } from "react-native-draggable-flatlist";
|
||||
@@ -14,18 +16,19 @@ import colors from "styles/tailwindColors";
|
||||
type Props = {
|
||||
evidenceList: Array<string>,
|
||||
handleAddEvidence?: Function,
|
||||
handleDragAndDrop: Function,
|
||||
showMediaViewer: Function,
|
||||
savingPhoto: boolean
|
||||
handleDragAndDrop: Function
|
||||
}
|
||||
|
||||
const EvidenceList = ( {
|
||||
evidenceList,
|
||||
handleAddEvidence,
|
||||
handleDragAndDrop,
|
||||
showMediaViewer,
|
||||
savingPhoto
|
||||
handleDragAndDrop
|
||||
}: Props ): Node => {
|
||||
const {
|
||||
savingPhoto,
|
||||
setSelectedPhotoIndex
|
||||
} = useContext( ObsEditContext );
|
||||
const navigation = useNavigation( );
|
||||
const imageClass = "h-16 w-16 justify-center mx-1.5 rounded-lg";
|
||||
|
||||
const renderPhoto = ( { item, getIndex, drag } ) => (
|
||||
@@ -33,7 +36,10 @@ const EvidenceList = ( {
|
||||
<Pressable
|
||||
onLongPress={drag}
|
||||
accessibilityRole="button"
|
||||
onPress={( ) => showMediaViewer( getIndex( ) )}
|
||||
onPress={( ) => {
|
||||
setSelectedPhotoIndex( getIndex( ) );
|
||||
navigation.navigate( "MediaViewer" );
|
||||
}}
|
||||
className={classnames( imageClass )}
|
||||
testID={`EvidenceList.${item.photo?.url || item.photo?.localFilePath}`}
|
||||
>
|
||||
|
||||
@@ -20,25 +20,21 @@ import AddEvidenceSheet from "./Sheets/AddEvidenceSheet";
|
||||
type Props = {
|
||||
passesEvidenceTest: Function,
|
||||
handleDragAndDrop: Function,
|
||||
showMediaViewer: Function,
|
||||
isFetchingLocation: boolean,
|
||||
locationTextClassNames: any,
|
||||
evidenceList: Array<string>,
|
||||
setShowAddEvidenceSheet: Function,
|
||||
showAddEvidenceSheet: boolean,
|
||||
savingPhoto: boolean
|
||||
showAddEvidenceSheet: boolean
|
||||
}
|
||||
|
||||
const EvidenceSection = ( {
|
||||
locationTextClassNames,
|
||||
handleDragAndDrop,
|
||||
showMediaViewer,
|
||||
passesEvidenceTest,
|
||||
isFetchingLocation,
|
||||
evidenceList,
|
||||
setShowAddEvidenceSheet,
|
||||
showAddEvidenceSheet,
|
||||
savingPhoto
|
||||
showAddEvidenceSheet
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const theme = useTheme( );
|
||||
@@ -103,8 +99,6 @@ const EvidenceSection = ( {
|
||||
evidenceList={evidenceList}
|
||||
handleAddEvidence={( ) => setShowAddEvidenceSheet( true )}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
showMediaViewer={showMediaViewer}
|
||||
savingPhoto={savingPhoto}
|
||||
/>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { DESIRED_LOCATION_ACCURACY } from "components/LocationPicker/LocationPicker";
|
||||
import {
|
||||
differenceInCalendarYears,
|
||||
@@ -13,7 +12,6 @@ import React, {
|
||||
useCallback, useContext, useEffect,
|
||||
useRef, useState
|
||||
} from "react";
|
||||
import Photo from "realmModels/Photo";
|
||||
import useCurrentObservationLocation from "sharedHooks/useCurrentObservationLocation";
|
||||
|
||||
import EvidenceSection from "./EvidenceSection";
|
||||
@@ -22,15 +20,12 @@ const EvidenceSectionContainer = ( ): Node => {
|
||||
const {
|
||||
currentObservation,
|
||||
setPassesEvidenceTest,
|
||||
savingPhoto,
|
||||
setMediaViewerUris,
|
||||
setSelectedPhotoIndex,
|
||||
updateObservationKeys
|
||||
updateObservationKeys,
|
||||
setPhotoEvidenceUris,
|
||||
photoEvidenceUris
|
||||
} = useContext( ObsEditContext );
|
||||
const obsPhotos = currentObservation?.observationPhotos;
|
||||
const mountedRef = useRef( true );
|
||||
const navigation = useNavigation( );
|
||||
const [deletePhotoMode, setDeletePhotoMode] = useState( false );
|
||||
|
||||
const [showAddEvidenceSheet, setShowAddEvidenceSheet] = useState( false );
|
||||
|
||||
@@ -50,6 +45,14 @@ const EvidenceSectionContainer = ( ): Node => {
|
||||
};
|
||||
}, [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( obsPhotos?.length > photoEvidenceUris?.length ) {
|
||||
setPhotoEvidenceUris( obsPhotos.map(
|
||||
obsPhoto => obsPhoto.photo?.url || obsPhoto.photo?.localFilePath
|
||||
) );
|
||||
}
|
||||
}, [obsPhotos, photoEvidenceUris, setPhotoEvidenceUris] );
|
||||
|
||||
const {
|
||||
hasLocation,
|
||||
isFetchingLocation
|
||||
@@ -96,16 +99,6 @@ const EvidenceSectionContainer = ( ): Node => {
|
||||
return false;
|
||||
}, [currentObservation] );
|
||||
|
||||
const showMediaViewer = index => {
|
||||
setSelectedPhotoIndex( index - 1 );
|
||||
setMediaViewerUris(
|
||||
obsPhotos.map(
|
||||
obsPhoto => Photo.displayLocalOrRemoteSquarePhoto( obsPhoto.photo )
|
||||
)
|
||||
);
|
||||
navigation.navigate( "MediaViewer" );
|
||||
};
|
||||
|
||||
const passesEvidenceTest = useCallback( ( ) => {
|
||||
if ( isFetchingLocation ) {
|
||||
return null;
|
||||
@@ -116,12 +109,6 @@ const EvidenceSectionContainer = ( ): Node => {
|
||||
return false;
|
||||
}, [isFetchingLocation, hasValidLocation, hasValidDate, hasPhotoOrSound] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( obsPhotos?.length === 0 && deletePhotoMode ) {
|
||||
setDeletePhotoMode( false );
|
||||
}
|
||||
}, [obsPhotos, deletePhotoMode] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
// we're only showing the Missing Evidence Sheet if location/date are missing
|
||||
// but not if there is a missing photo or sound
|
||||
@@ -142,19 +129,19 @@ const EvidenceSectionContainer = ( ): Node => {
|
||||
updateObservationKeys( {
|
||||
observationPhotos: data
|
||||
} );
|
||||
const uris = data.map( obsPhoto => obsPhoto.photo?.url || obsPhoto.photo?.localFilePath );
|
||||
setPhotoEvidenceUris( uris );
|
||||
};
|
||||
|
||||
return (
|
||||
<EvidenceSection
|
||||
locationTextClassNames={locationTextClassNames}
|
||||
handleDragAndDrop={handleDragAndDrop}
|
||||
showMediaViewer={showMediaViewer}
|
||||
passesEvidenceTest={passesEvidenceTest}
|
||||
isFetchingLocation={isFetchingLocation}
|
||||
evidenceList={obsPhotos || []}
|
||||
setShowAddEvidenceSheet={setShowAddEvidenceSheet}
|
||||
showAddEvidenceSheet={showAddEvidenceSheet}
|
||||
savingPhoto={savingPhoto}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,8 @@ const IdentificationSection = ( ): Node => {
|
||||
const {
|
||||
currentObservation,
|
||||
updateObservationKeys,
|
||||
setPassesIdentificationTest
|
||||
setPassesIdentificationTest,
|
||||
setLastScreen
|
||||
} = useContext( ObsEditContext );
|
||||
const { t } = useTranslation( );
|
||||
const theme = useTheme( );
|
||||
@@ -49,7 +50,10 @@ const IdentificationSection = ( ): Node => {
|
||||
} );
|
||||
};
|
||||
|
||||
const navToAddID = ( ) => navigation.navigate( "AddID" );
|
||||
const navToSuggestions = ( ) => {
|
||||
setLastScreen( "ObsEdit" );
|
||||
navigation.navigate( "Suggestions" );
|
||||
};
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( hasIdentification ) {
|
||||
@@ -72,13 +76,13 @@ const IdentificationSection = ( ): Node => {
|
||||
<View className="flex-row items-center justify-between mr-5 mt-5">
|
||||
<DisplayTaxon
|
||||
taxon={identification}
|
||||
handlePress={navToAddID}
|
||||
handlePress={navToSuggestions}
|
||||
accessibilityLabel={t( "Navigates-to-add-identification" )}
|
||||
/>
|
||||
<INatIconButton
|
||||
icon="edit"
|
||||
size={20}
|
||||
onPress={navToAddID}
|
||||
onPress={navToSuggestions}
|
||||
accessibilityLabel={t( "Edit" )}
|
||||
accessibilityHint={t( "Navigates-to-add-identification" )}
|
||||
/>
|
||||
@@ -96,7 +100,7 @@ const IdentificationSection = ( ): Node => {
|
||||
level={identification
|
||||
? "primary"
|
||||
: "focus"}
|
||||
onPress={navToAddID}
|
||||
onPress={navToSuggestions}
|
||||
text={t( "ADD-AN-ID" )}
|
||||
className={classnames( "rounded-full py-1 h-[36px]", {
|
||||
"border border-darkGray border-[2px]": identification
|
||||
|
||||
@@ -18,7 +18,7 @@ const ConfidenceInterval = ( { confidence = 0, activeColor = "bg-white" }: Props
|
||||
)}
|
||||
/>
|
||||
);
|
||||
const Dot = <View className="bg-lightGray h-[2px] w-[2px] rounded-full mx-[1px]" />;
|
||||
const Dot = <View className="bg-mediumGray h-[2px] w-[2px] rounded-full mx-[1px]" />;
|
||||
|
||||
const dots = [];
|
||||
|
||||
@@ -29,7 +29,7 @@ const ConfidenceInterval = ( { confidence = 0, activeColor = "bg-white" }: Props
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-row justify-evenly items-center w-[36px]">
|
||||
<View className="flex-row justify-between items-center w-[36px]">
|
||||
{dots}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -33,76 +33,77 @@
|
||||
"crop": 61728,
|
||||
"currentlocation": 61729,
|
||||
"door-exit": 61730,
|
||||
"flag": 61731,
|
||||
"flash-off": 61732,
|
||||
"flash-on": 61733,
|
||||
"flip": 61734,
|
||||
"gallery": 61735,
|
||||
"gear": 61736,
|
||||
"globe-outline": 61737,
|
||||
"grid-square": 61738,
|
||||
"grid": 61739,
|
||||
"hamburger-menu": 61740,
|
||||
"heart": 61741,
|
||||
"help-circle-outline": 61742,
|
||||
"help-circle": 61743,
|
||||
"help": 61744,
|
||||
"iconic-actinopterygii": 61745,
|
||||
"iconic-amphibia": 61746,
|
||||
"iconic-animalia": 61747,
|
||||
"iconic-arachnida": 61748,
|
||||
"iconic-aves": 61749,
|
||||
"iconic-chromista": 61750,
|
||||
"iconic-fungi": 61751,
|
||||
"iconic-insecta": 61752,
|
||||
"iconic-mammalia": 61753,
|
||||
"iconic-mollusca": 61754,
|
||||
"iconic-plantae": 61755,
|
||||
"iconic-protozoa": 61756,
|
||||
"iconic-reptilia": 61757,
|
||||
"iconic-unknown": 61758,
|
||||
"id-agree": 61759,
|
||||
"inaturalist": 61760,
|
||||
"info-circle-outline": 61761,
|
||||
"kebab-menu": 61762,
|
||||
"label-outline": 61763,
|
||||
"label": 61764,
|
||||
"laptop": 61765,
|
||||
"layers": 61766,
|
||||
"leaf": 61767,
|
||||
"list-square": 61768,
|
||||
"location-crosshairs": 61769,
|
||||
"magnifying-glass": 61770,
|
||||
"map-layers": 61771,
|
||||
"map-marker-outline": 61772,
|
||||
"map": 61773,
|
||||
"microphone-circle": 61774,
|
||||
"microphone": 61775,
|
||||
"noevidence": 61776,
|
||||
"notifications-bell": 61777,
|
||||
"pause-circle": 61778,
|
||||
"pencil-outline": 61779,
|
||||
"pencil": 61780,
|
||||
"person": 61781,
|
||||
"photos-outline": 61782,
|
||||
"photos": 61783,
|
||||
"play-circle": 61784,
|
||||
"play": 61785,
|
||||
"plus-bold": 61786,
|
||||
"plus": 61787,
|
||||
"pot-outline": 61788,
|
||||
"rotate-exclamation": 61789,
|
||||
"rotate-right": 61790,
|
||||
"rotate": 61791,
|
||||
"share": 61792,
|
||||
"sliders": 61793,
|
||||
"sound-bold-outline": 61794,
|
||||
"sound-outline": 61795,
|
||||
"sounds": 61796,
|
||||
"sparkly-label": 61797,
|
||||
"star-bold-outline": 61798,
|
||||
"star": 61799,
|
||||
"trash-outline": 61800,
|
||||
"trash": 61801,
|
||||
"triangle-exclamation": 61802
|
||||
"edit-comment": 61731,
|
||||
"flag": 61732,
|
||||
"flash-off": 61733,
|
||||
"flash-on": 61734,
|
||||
"flip": 61735,
|
||||
"gallery": 61736,
|
||||
"gear": 61737,
|
||||
"globe-outline": 61738,
|
||||
"grid-square": 61739,
|
||||
"grid": 61740,
|
||||
"hamburger-menu": 61741,
|
||||
"heart": 61742,
|
||||
"help-circle-outline": 61743,
|
||||
"help-circle": 61744,
|
||||
"help": 61745,
|
||||
"iconic-actinopterygii": 61746,
|
||||
"iconic-amphibia": 61747,
|
||||
"iconic-animalia": 61748,
|
||||
"iconic-arachnida": 61749,
|
||||
"iconic-aves": 61750,
|
||||
"iconic-chromista": 61751,
|
||||
"iconic-fungi": 61752,
|
||||
"iconic-insecta": 61753,
|
||||
"iconic-mammalia": 61754,
|
||||
"iconic-mollusca": 61755,
|
||||
"iconic-plantae": 61756,
|
||||
"iconic-protozoa": 61757,
|
||||
"iconic-reptilia": 61758,
|
||||
"iconic-unknown": 61759,
|
||||
"id-agree": 61760,
|
||||
"inaturalist": 61761,
|
||||
"info-circle-outline": 61762,
|
||||
"kebab-menu": 61763,
|
||||
"label-outline": 61764,
|
||||
"label": 61765,
|
||||
"laptop": 61766,
|
||||
"layers": 61767,
|
||||
"leaf": 61768,
|
||||
"list-square": 61769,
|
||||
"location-crosshairs": 61770,
|
||||
"magnifying-glass": 61771,
|
||||
"map-layers": 61772,
|
||||
"map-marker-outline": 61773,
|
||||
"map": 61774,
|
||||
"microphone-circle": 61775,
|
||||
"microphone": 61776,
|
||||
"noevidence": 61777,
|
||||
"notifications-bell": 61778,
|
||||
"pause-circle": 61779,
|
||||
"pencil-outline": 61780,
|
||||
"pencil": 61781,
|
||||
"person": 61782,
|
||||
"photos-outline": 61783,
|
||||
"photos": 61784,
|
||||
"play-circle": 61785,
|
||||
"play": 61786,
|
||||
"plus-bold": 61787,
|
||||
"plus": 61788,
|
||||
"pot-outline": 61789,
|
||||
"rotate-exclamation": 61790,
|
||||
"rotate-right": 61791,
|
||||
"rotate": 61792,
|
||||
"share": 61793,
|
||||
"sliders": 61794,
|
||||
"sound-bold-outline": 61795,
|
||||
"sound-outline": 61796,
|
||||
"sounds": 61797,
|
||||
"sparkly-label": 61798,
|
||||
"star-bold-outline": 61799,
|
||||
"star": 61800,
|
||||
"trash-outline": 61801,
|
||||
"trash": 61802,
|
||||
"triangle-exclamation": 61803
|
||||
}
|
||||
@@ -26,8 +26,9 @@ type Props = {
|
||||
selected?: boolean,
|
||||
source: SOURCE,
|
||||
style?: Object,
|
||||
testID?: string,
|
||||
white?: boolean,
|
||||
className?: string,
|
||||
testID?: string,
|
||||
width?: string
|
||||
};
|
||||
|
||||
@@ -44,6 +45,7 @@ const ObsImagePreview = ( {
|
||||
selected = false,
|
||||
source,
|
||||
style,
|
||||
className,
|
||||
testID,
|
||||
white = false,
|
||||
width = "w-[62px]"
|
||||
@@ -59,6 +61,8 @@ const ObsImagePreview = ( {
|
||||
"relative",
|
||||
borderRadius,
|
||||
height,
|
||||
borderRadius,
|
||||
className,
|
||||
width
|
||||
);
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ const SearchBar = ( {
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<View className="absolute right-4 top-[20px]">
|
||||
<View className="absolute right-4 top-[15px]">
|
||||
<INatIcon name="magnifying-glass" size={18} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,10 @@ type Props = {
|
||||
testID: string,
|
||||
clearBackground?: boolean,
|
||||
confidence?: number,
|
||||
white?: boolean
|
||||
white?: boolean,
|
||||
activeColor?: string,
|
||||
confidencePosition?: string,
|
||||
first?: boolean,
|
||||
};
|
||||
|
||||
const TaxonResult = ( {
|
||||
@@ -27,21 +30,29 @@ const TaxonResult = ( {
|
||||
handleCheckmarkPress,
|
||||
taxon,
|
||||
testID,
|
||||
white = false
|
||||
white = false,
|
||||
activeColor,
|
||||
confidencePosition = "photo",
|
||||
first = false
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const navigation = useNavigation( );
|
||||
const theme = useTheme( );
|
||||
const taxonImage = { uri: taxon?.default_photo?.url };
|
||||
|
||||
const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", {
|
||||
id: taxon.id
|
||||
} );
|
||||
|
||||
return (
|
||||
<View
|
||||
className={
|
||||
classnames(
|
||||
"flex-row items-center justify-between pl-3 py-1",
|
||||
"flex-row items-center justify-between px-4 py-3",
|
||||
{
|
||||
"border-[0.5px] border-lightGray": !clearBackground,
|
||||
"mx-4": clearBackground
|
||||
"border-b-[1px] border-lightGray": !clearBackground,
|
||||
"mx-4": clearBackground,
|
||||
"border-t-[1px]": first
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -49,7 +60,7 @@ const TaxonResult = ( {
|
||||
>
|
||||
<Pressable
|
||||
className="flex-row items-center w-16 grow"
|
||||
onPress={() => navigation.navigate( "TaxonDetails", { id: taxon.id } )}
|
||||
onPress={navToTaxonDetails}
|
||||
accessible
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel={t( "Navigate-to-taxon-details" )}
|
||||
@@ -60,12 +71,16 @@ const TaxonResult = ( {
|
||||
source={taxonImage}
|
||||
testID={`${testID}.photo`}
|
||||
iconicTaxonName={taxon?.iconic_taxon_name}
|
||||
className="rounded-xl"
|
||||
isSmall
|
||||
white={white}
|
||||
/>
|
||||
{confidence && (
|
||||
{( confidence && confidencePosition === "photo" ) && (
|
||||
<View className="absolute -bottom-5 w-[62px]">
|
||||
<ConfidenceInterval confidence={confidence} />
|
||||
<ConfidenceInterval
|
||||
confidence={confidence}
|
||||
activeColor={activeColor}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<View className="shrink ml-3">
|
||||
@@ -74,28 +89,27 @@ const TaxonResult = ( {
|
||||
layout="horizontal"
|
||||
color={clearBackground && "text-white"}
|
||||
/>
|
||||
{( confidence && confidencePosition === "text" ) && (
|
||||
<View className="absolute -bottom-3 w-[62px]">
|
||||
<ConfidenceInterval
|
||||
confidence={confidence}
|
||||
activeColor={activeColor}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
<View className="flex-row items-center">
|
||||
<INatIconButton
|
||||
icon="info-circle-outline"
|
||||
size={22}
|
||||
onPress={() => navigation.navigate( "TabNavigator", {
|
||||
screen: "ObservationsStackNavigator",
|
||||
params: {
|
||||
screen: "TaxonDetails",
|
||||
params: {
|
||||
id: taxon.id,
|
||||
lastScreen: "AddID"
|
||||
}
|
||||
}
|
||||
} )}
|
||||
onPress={navToTaxonDetails}
|
||||
color={clearBackground && theme.colors.onSecondary}
|
||||
accessibilityLabel={t( "Information" )}
|
||||
accessibilityHint={t( "Navigate-to-taxon-details" )}
|
||||
/>
|
||||
<INatIconButton
|
||||
className="mx-2"
|
||||
className="ml-2"
|
||||
icon={clearBackground
|
||||
? "checkmark-circle-outline"
|
||||
: "checkmark-circle"}
|
||||
@@ -106,6 +120,7 @@ const TaxonResult = ( {
|
||||
onPress={handleCheckmarkPress}
|
||||
accessibilityLabel={t( "Checkmark" )}
|
||||
accessibilityHint={t( "Add-this-ID" )}
|
||||
testID={`${testID}.checkmark`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { default as ActivityCount } from "./ActivityCount/ActivityCount";
|
||||
export { default as CommentsCount } from "./ActivityCount/CommentsCount";
|
||||
export { default as IdentificationsCount } from "./ActivityCount/IdentificationsCount";
|
||||
export { default as BackButton } from "./Buttons/BackButton";
|
||||
export { default as Button } from "./Buttons/Button";
|
||||
export { default as CloseButton } from "./Buttons/CloseButton";
|
||||
export { default as EvidenceButton } from "./Buttons/EvidenceButton";
|
||||
|
||||
53
src/components/Suggestions/AddCommentPrompt.js
Normal file
53
src/components/Suggestions/AddCommentPrompt.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {
|
||||
INatIconButton,
|
||||
TextInputSheet
|
||||
} from "components/SharedComponents";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useLocalObservation, useTranslation } from "sharedHooks";
|
||||
|
||||
const AddCommentPrompt = ( ): Node => {
|
||||
const {
|
||||
setComment,
|
||||
currentObservation
|
||||
} = useContext( ObsEditContext );
|
||||
const [showAddCommentSheet, setShowAddCommentSheet] = useState( false );
|
||||
const uuid = currentObservation?.uuid;
|
||||
const localObservation = useLocalObservation( uuid );
|
||||
const wasSynced = localObservation?.wasSynced( );
|
||||
|
||||
const { t } = useTranslation( );
|
||||
const navigation = useNavigation( );
|
||||
|
||||
useEffect( ( ) => {
|
||||
const addCommentIcon = ( ) => (
|
||||
<INatIconButton
|
||||
icon="edit-comment"
|
||||
onPress={( ) => setShowAddCommentSheet( true )}
|
||||
accessibilityLabel={t( "Add-comment" )}
|
||||
size={25}
|
||||
/>
|
||||
);
|
||||
|
||||
if ( wasSynced ) {
|
||||
navigation.setOptions( {
|
||||
headerRight: addCommentIcon
|
||||
} );
|
||||
}
|
||||
}, [navigation, t, wasSynced] );
|
||||
|
||||
return showAddCommentSheet && (
|
||||
<TextInputSheet
|
||||
handleClose={( ) => setShowAddCommentSheet( false )}
|
||||
headerText={t( "ADD-OPTIONAL-COMMENT" )}
|
||||
snapPoints={[416]}
|
||||
confirm={textInput => setComment( textInput )}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCommentPrompt;
|
||||
58
src/components/Suggestions/Attribution.js
Normal file
58
src/components/Suggestions/Attribution.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// @flow
|
||||
|
||||
import { fetchObservers } from "api/observations";
|
||||
import {
|
||||
Body3
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
useAuthenticatedQuery,
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
|
||||
type Props = {
|
||||
taxonIds: Array<number>
|
||||
};
|
||||
|
||||
const Attribution = ( {
|
||||
taxonIds
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const { data } = useAuthenticatedQuery(
|
||||
["fetchObservers", taxonIds],
|
||||
( ) => fetchObservers( {
|
||||
taxon_ids: taxonIds,
|
||||
per_page: 3,
|
||||
fields: {
|
||||
user: {
|
||||
login: true,
|
||||
name: true
|
||||
}
|
||||
}
|
||||
} ),
|
||||
|
||||
{
|
||||
enabled: taxonIds.length > 0
|
||||
}
|
||||
);
|
||||
|
||||
const observers = data?.results?.map( observation => observation.user.login );
|
||||
|
||||
if ( !observers || observers?.length === 0 ) {
|
||||
return <View testID="Attribution.empty" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Body3 className="mt-6 mb-4 mx-4">
|
||||
{t( "iNaturalist-Identification-suggestions-are-trained-on", {
|
||||
user1: observers[0],
|
||||
user2: observers[1],
|
||||
user3: observers[2]
|
||||
} )}
|
||||
</Body3>
|
||||
);
|
||||
};
|
||||
|
||||
export default Attribution;
|
||||
38
src/components/Suggestions/CommentBox.js
Normal file
38
src/components/Suggestions/CommentBox.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
Body3, INatIcon
|
||||
} from "components/SharedComponents";
|
||||
import {
|
||||
View
|
||||
} from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
type Props = {
|
||||
comment: string
|
||||
};
|
||||
|
||||
const CommentBox = ( {
|
||||
comment
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
|
||||
return comment && comment.length > 0 && (
|
||||
<>
|
||||
<Body3 className="my-5 mx-8">
|
||||
{t( "Your-identification-will-be-posted-with-the-following-comment" )}
|
||||
</Body3>
|
||||
<View className="bg-lightGray mx-6 p-5 rounded-lg flex-row items-center">
|
||||
<INatIcon
|
||||
name="add-comment-outline"
|
||||
size={22}
|
||||
/>
|
||||
<Body3 className="ml-4 shrink">{ comment }</Body3>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentBox;
|
||||
62
src/components/Suggestions/PhotoSelectionList.js
Normal file
62
src/components/Suggestions/PhotoSelectionList.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// @flow
|
||||
|
||||
import classnames from "classnames";
|
||||
import {
|
||||
Image, Pressable, View
|
||||
} from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
type Props = {
|
||||
photoUris: Array<string>,
|
||||
selectedPhotoUri: string,
|
||||
setSelectedPhotoUri: Function
|
||||
};
|
||||
|
||||
const PhotoSelectionList = ( {
|
||||
photoUris, selectedPhotoUri, setSelectedPhotoUri
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const renderPhoto = ( { item } ) => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={( ) => {
|
||||
setSelectedPhotoUri( item );
|
||||
}}
|
||||
className={classnames(
|
||||
"w-[83px] h-[83px] justify-center mx-1.5 rounded-lg"
|
||||
)}
|
||||
accessibilityLabel={t( "Select-photo" )}
|
||||
testID={`PhotoSelectionList.${item}`}
|
||||
>
|
||||
<View
|
||||
className={classnames(
|
||||
"rounded-lg overflow-hidden",
|
||||
{
|
||||
"border border-inatGreen border-[3px]": selectedPhotoUri === item
|
||||
}
|
||||
)}
|
||||
testID={`PhotoSelectionList.border.${item}`}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item }}
|
||||
accessibilityIgnoresInvertColors
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={photoUris}
|
||||
renderItem={renderPhoto}
|
||||
horizontal
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhotoSelectionList;
|
||||
80
src/components/Suggestions/Suggestions.js
Normal file
80
src/components/Suggestions/Suggestions.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {
|
||||
Body3, Button, ScrollViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
import {
|
||||
View
|
||||
} from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityIndicator
|
||||
} from "react-native-paper";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
import AddCommentPrompt from "./AddCommentPrompt";
|
||||
import Attribution from "./Attribution";
|
||||
import CommentBox from "./CommentBox";
|
||||
import PhotoSelectionList from "./PhotoSelectionList";
|
||||
import SuggestionsList from "./SuggestionsList";
|
||||
|
||||
type Props = {
|
||||
comment: string,
|
||||
photoUris: Array<string>,
|
||||
selectedPhotoUri: string,
|
||||
setSelectedPhotoUri: Function,
|
||||
nearbySuggestions: Array<Object>,
|
||||
onTaxonChosen: Function,
|
||||
loading: boolean,
|
||||
loadingSuggestions: boolean
|
||||
};
|
||||
|
||||
const Suggestions = ( {
|
||||
photoUris, selectedPhotoUri, setSelectedPhotoUri, nearbySuggestions, onTaxonChosen,
|
||||
comment, loading, loadingSuggestions
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const navigation = useNavigation( );
|
||||
|
||||
return (
|
||||
<ScrollViewWrapper testID="suggestions">
|
||||
<AddCommentPrompt />
|
||||
{loading && (
|
||||
<View
|
||||
className="absolute self-center z-10 pt-[30px]"
|
||||
testID="Suggestions.ActivityIndicator"
|
||||
>
|
||||
<ActivityIndicator large />
|
||||
</View>
|
||||
)}
|
||||
<View className="mx-5">
|
||||
<PhotoSelectionList
|
||||
photoUris={photoUris}
|
||||
selectedPhotoUri={selectedPhotoUri}
|
||||
setSelectedPhotoUri={setSelectedPhotoUri}
|
||||
/>
|
||||
<Body3 className="my-4 mx-3">{t( "Select-the-identification-you-want-to-add" )}</Body3>
|
||||
<Button
|
||||
text={t( "SEARCH-FOR-A-TAXON" )}
|
||||
onPress={( ) => navigation.navigate( "TaxonSearch" )}
|
||||
accessibilityLabel={t( "Search" )}
|
||||
/>
|
||||
</View>
|
||||
<CommentBox comment={comment} />
|
||||
<SuggestionsList
|
||||
nearbySuggestions={nearbySuggestions}
|
||||
onTaxonChosen={onTaxonChosen}
|
||||
loading={loadingSuggestions}
|
||||
/>
|
||||
{nearbySuggestions?.length > 0 && (
|
||||
<Attribution
|
||||
taxonIds={nearbySuggestions.map( suggestion => suggestion.taxon.id )}
|
||||
/>
|
||||
)}
|
||||
</ScrollViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Suggestions;
|
||||
74
src/components/Suggestions/SuggestionsContainer.js
Normal file
74
src/components/Suggestions/SuggestionsContainer.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// @flow
|
||||
|
||||
import scoreImage from "api/computerVision";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import flattenUploadParams from "sharedHelpers/flattenUploadParams";
|
||||
import {
|
||||
useAuthenticatedQuery,
|
||||
useLocalObservation
|
||||
} from "sharedHooks";
|
||||
|
||||
import Suggestions from "./Suggestions";
|
||||
|
||||
const SuggestionsContainer = ( ): Node => {
|
||||
const {
|
||||
photoEvidenceUris,
|
||||
currentObservation,
|
||||
createId,
|
||||
loading,
|
||||
comment,
|
||||
setPhotoEvidenceUris
|
||||
} = useContext( ObsEditContext );
|
||||
const uuid = currentObservation?.uuid;
|
||||
const obsPhotos = currentObservation?.observationPhotos;
|
||||
const localObservation = useLocalObservation( uuid );
|
||||
const [selectedPhotoUri, setSelectedPhotoUri] = useState( photoEvidenceUris[0] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( obsPhotos?.length > photoEvidenceUris?.length ) {
|
||||
setPhotoEvidenceUris( obsPhotos.map(
|
||||
obsPhoto => obsPhoto.photo?.url || obsPhoto.photo?.localFilePath
|
||||
) );
|
||||
}
|
||||
}, [obsPhotos, photoEvidenceUris, setPhotoEvidenceUris] );
|
||||
|
||||
const params = {
|
||||
image: selectedPhotoUri,
|
||||
latitude: localObservation?.latitude || currentObservation?.latitude,
|
||||
longitude: localObservation?.longitude || currentObservation?.longitude
|
||||
};
|
||||
|
||||
const { data: nearbySuggestions, isLoading: loadingSuggestions } = useAuthenticatedQuery(
|
||||
["scoreImage", selectedPhotoUri],
|
||||
async optsWithAuth => scoreImage(
|
||||
await flattenUploadParams(
|
||||
params.image,
|
||||
params.latitude,
|
||||
params.longitude
|
||||
),
|
||||
optsWithAuth
|
||||
),
|
||||
{
|
||||
enabled: !!selectedPhotoUri
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Suggestions
|
||||
photoUris={photoEvidenceUris}
|
||||
selectedPhotoUri={selectedPhotoUri}
|
||||
setSelectedPhotoUri={setSelectedPhotoUri}
|
||||
onTaxonChosen={
|
||||
identification => createId( identification )
|
||||
}
|
||||
comment={comment}
|
||||
loading={loading}
|
||||
nearbySuggestions={nearbySuggestions}
|
||||
loadingSuggestions={loadingSuggestions && photoEvidenceUris.length > 0}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsContainer;
|
||||
104
src/components/Suggestions/SuggestionsList.js
Normal file
104
src/components/Suggestions/SuggestionsList.js
Normal file
@@ -0,0 +1,104 @@
|
||||
// @flow
|
||||
|
||||
import { Body1, Heading4, TaxonResult } from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { ActivityIndicator } from "react-native-paper";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
type Props = {
|
||||
nearbySuggestions: Array<Object>,
|
||||
onTaxonChosen: Function,
|
||||
loading: boolean
|
||||
};
|
||||
|
||||
const SuggestionsList = ( {
|
||||
nearbySuggestions,
|
||||
onTaxonChosen,
|
||||
loading
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
|
||||
if ( loading ) {
|
||||
return (
|
||||
<View className="justify-center items-center mt-5" testID="SuggestionsList.loading">
|
||||
<ActivityIndicator large />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if ( !nearbySuggestions || nearbySuggestions.length === 0 ) {
|
||||
return (
|
||||
<>
|
||||
<Heading4 className="mt-6 mb-4 ml-4">{t( "TOP-ID-SUGGESTION" )}</Heading4>
|
||||
<Body1 className="mx-10 text-center">
|
||||
{t( "iNaturalist-isnt-able-to-provide-a-top-ID-suggestion-for-this-photo" )}
|
||||
</Body1>
|
||||
<Heading4 className="mt-6 mb-4 ml-4">{t( "NEARBY-SUGGESTIONS" )}</Heading4>
|
||||
<Body1 className="mx-10 text-center">
|
||||
{t( "iNaturalist-has-no-ID-suggestions-for-this-photo" )}
|
||||
</Body1>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const topSuggestion = nearbySuggestions[0];
|
||||
|
||||
const convertScoreToConfidence = score => {
|
||||
if ( !score ) {
|
||||
return null;
|
||||
}
|
||||
if ( score < 20 ) {
|
||||
return 1;
|
||||
}
|
||||
if ( score < 40 ) {
|
||||
return 2;
|
||||
}
|
||||
if ( score < 60 ) {
|
||||
return 3;
|
||||
}
|
||||
if ( score < 80 ) {
|
||||
return 4;
|
||||
}
|
||||
return 5;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading4 className="mt-6 mb-4 ml-4">{t( "TOP-ID-SUGGESTION" )}</Heading4>
|
||||
<View className="bg-inatGreen/[.13]">
|
||||
<TaxonResult
|
||||
key={topSuggestion.taxon.id}
|
||||
taxon={topSuggestion.taxon}
|
||||
handleCheckmarkPress={( ) => onTaxonChosen( topSuggestion.taxon )}
|
||||
testID={`SuggestionsList.taxa.${topSuggestion.taxon.id}`}
|
||||
confidence={convertScoreToConfidence( topSuggestion.combined_score )}
|
||||
activeColor="bg-inatGreen"
|
||||
confidencePosition="text"
|
||||
first
|
||||
/>
|
||||
</View>
|
||||
<Heading4 className="mt-6 mb-4 ml-4">{t( "NEARBY-SUGGESTIONS" )}</Heading4>
|
||||
{nearbySuggestions.map( ( suggestion, index ) => {
|
||||
if ( index === 0 ) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TaxonResult
|
||||
key={suggestion.taxon.id}
|
||||
taxon={suggestion.taxon}
|
||||
handleCheckmarkPress={( ) => onTaxonChosen( suggestion.taxon )}
|
||||
testID={`SuggestionsList.taxa.${suggestion.taxon.id}`}
|
||||
confidence={convertScoreToConfidence( suggestion.combined_score )}
|
||||
activeColor="bg-inatGreen"
|
||||
confidencePosition="text"
|
||||
first={index === 0}
|
||||
/>
|
||||
);
|
||||
} )}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsList;
|
||||
83
src/components/Suggestions/TaxonSearch.js
Normal file
83
src/components/Suggestions/TaxonSearch.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import fetchSearchResults from "api/search";
|
||||
import {
|
||||
SearchBar,
|
||||
TaxonResult,
|
||||
ViewWrapper
|
||||
} from "components/SharedComponents";
|
||||
import { View } from "components/styledComponents";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import Taxon from "realmModels/Taxon";
|
||||
import { useAuthenticatedQuery } from "sharedHooks";
|
||||
|
||||
import AddCommentPrompt from "./AddCommentPrompt";
|
||||
import CommentBox from "./CommentBox";
|
||||
|
||||
const TaxonSearch = ( ): Node => {
|
||||
const {
|
||||
createId,
|
||||
comment
|
||||
} = useContext( ObsEditContext );
|
||||
const [taxonQuery, setTaxonQuery] = useState( "" );
|
||||
const navigation = useNavigation( );
|
||||
const { data: taxonList } = useAuthenticatedQuery(
|
||||
["fetchSearchResults", taxonQuery],
|
||||
optsWithAuth => fetchSearchResults(
|
||||
{
|
||||
q: taxonQuery,
|
||||
sources: "taxa",
|
||||
fields: {
|
||||
taxon: Taxon.TAXON_FIELDS
|
||||
}
|
||||
},
|
||||
optsWithAuth
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(
|
||||
( ) => {
|
||||
navigation.addListener( "blur", ( ) => {
|
||||
setTaxonQuery( "" );
|
||||
} );
|
||||
},
|
||||
[navigation]
|
||||
);
|
||||
|
||||
const renderFooter = ( ) => (
|
||||
<View className="pb-10" />
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper className="flex-1">
|
||||
<AddCommentPrompt />
|
||||
<CommentBox comment={comment} />
|
||||
<SearchBar
|
||||
handleTextChange={setTaxonQuery}
|
||||
value={taxonQuery}
|
||||
testID="SearchTaxon"
|
||||
containerClass="my-5 mx-4"
|
||||
/>
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
data={taxonList}
|
||||
renderItem={( { item, index } ) => (
|
||||
<TaxonResult
|
||||
taxon={item}
|
||||
handleCheckmarkPress={( ) => createId( item )}
|
||||
testID={`Search.taxa.${item.id}`}
|
||||
first={index === 0}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => item.id}
|
||||
ListFooterComponent={renderFooter}
|
||||
/>
|
||||
</ViewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaxonSearch;
|
||||
@@ -4,6 +4,7 @@ import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { fetchTaxon } from "api/taxa";
|
||||
import PlaceholderText from "components/PlaceholderText";
|
||||
import {
|
||||
BackButton,
|
||||
DisplayTaxonName,
|
||||
Heading4,
|
||||
HideView,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
ScrollViewWrapper,
|
||||
Tabs
|
||||
} from "components/SharedComponents";
|
||||
import BackButton from "components/SharedComponents/Buttons/BackButton";
|
||||
import { ImageBackground, View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
@@ -29,7 +29,6 @@ const TaxonDetails = ( ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const { params } = useRoute( );
|
||||
const { id } = params;
|
||||
const lastScreen = params?.lastScreen;
|
||||
const { t } = useTranslation( );
|
||||
const [currentTabId, setCurrentTabId] = useState( ABOUT_TAB_ID );
|
||||
|
||||
@@ -70,16 +69,7 @@ const TaxonDetails = ( ): Node => {
|
||||
<View className="absolute left-5 top-5">
|
||||
<BackButton
|
||||
color={theme.colors.onPrimary}
|
||||
onPress={( ) => {
|
||||
if ( lastScreen ) {
|
||||
navigation.navigate( "CameraNavigator", {
|
||||
screen: "AddID",
|
||||
params: { }
|
||||
} );
|
||||
} else {
|
||||
navigation.goBack( );
|
||||
}
|
||||
}}
|
||||
onPress={( ) => navigation.goBack( )}
|
||||
/>
|
||||
</View>
|
||||
<View className="absolute bottom-5 left-5">
|
||||
@@ -93,7 +83,12 @@ const TaxonDetails = ( ): Node => {
|
||||
<View className="absolute bottom-5 right-5">
|
||||
<INatIconButton
|
||||
icon="compass-rose-outline"
|
||||
onPress={( ) => navigation.navigate( "Explore" )}
|
||||
onPress={( ) => navigation.navigate( "TabNavigator", {
|
||||
screen: "ObservationsStackNavigator",
|
||||
params: {
|
||||
screen: "Explore"
|
||||
}
|
||||
} )}
|
||||
accessibilityLabel={t( "Explore" )}
|
||||
accessibilityHint={t( "Navigates-to-explore" )}
|
||||
size={30}
|
||||
|
||||
@@ -357,7 +357,7 @@ IDENTIFICATION = IDENTIFICATION
|
||||
|
||||
IDENTIFICATIONS = IDENTIFICATIONS
|
||||
|
||||
If-an-account-with-that-email-exists = If an account with that email exists, we’ve sent password reset instructions to your email.
|
||||
If-an-account-with-that-email-exists = If an account with that email exists, we've sent password reset instructions to your email.
|
||||
|
||||
# Shows the number of photos a user selected from the camera roll for upload
|
||||
Import-X-photos = Import {$count ->
|
||||
@@ -704,8 +704,6 @@ Search-for-a-project = Search for a project
|
||||
|
||||
Search-for-a-taxon = Search for a taxon
|
||||
|
||||
Search-for-a-taxon-to-add-an-identification = Search for a taxon to add an identification.
|
||||
|
||||
Search-for-a-user = Search for a user
|
||||
|
||||
Search-for-description-tags-text = Search for description/tags text
|
||||
@@ -846,7 +844,7 @@ VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
|
||||
|
||||
View-in-browser = View in Browser
|
||||
|
||||
Visually-search-iNaturalist-data = Visually search iNaturalist’s wealth of data. Search by a taxon in a location
|
||||
Visually-search-iNaturalist-data = Visually search iNaturalist's wealth of data. Search by a taxon in a location
|
||||
|
||||
Welcome-to-iNaturalist = Welcome to iNaturalist!
|
||||
|
||||
@@ -916,7 +914,7 @@ Take-a-photo-with-your-camera = Take a photo with your camera
|
||||
Upload-a-photo-from-your-gallery = Upload a photo from your gallery
|
||||
Record-a-sound = Record a sound
|
||||
|
||||
You-can-also-explore-existing-observations = You can also explore existing observations on iNaturalist to discover what’s around you.
|
||||
You-can-also-explore-existing-observations = You can also explore existing observations on iNaturalist to discover what's around you.
|
||||
|
||||
# Message shown when a permission is required to use a part of the app
|
||||
# (e.g. permission to access the camera) but the user denied the permission.
|
||||
@@ -934,7 +932,7 @@ You-will-lose-all-existing-observations = {$count ->
|
||||
You-can-still-share-the-file =
|
||||
You can still share the file with another app. If you can email it, please send it to { $email }
|
||||
|
||||
Zoom-in = Zoom in so that the observation’s accuracy is as low as possible.
|
||||
Zoom-in = Zoom in so that the observation's accuracy is as low as possible.
|
||||
|
||||
Your-location-uncertainty-is-over-4000km = Your location uncertainty is over 4000km, which is too high to be helpful to identifiers. Edit the location and zoom in until the accuracy circle turns green and is centered on where you observed the organism.
|
||||
|
||||
@@ -1085,6 +1083,8 @@ Location-map-unavailable-without-internet = Location map unavailable without int
|
||||
Observation-photos-unavailable-without-internet = Observation photos unavailable without internet
|
||||
Taxon-photo-unavailable-without-internet = Taxon photo unavailable without internet
|
||||
User-photo-unavailable-without-internet = User photo unavailable without internet
|
||||
Search = Search
|
||||
Select-photo = Select photo
|
||||
|
||||
# Accessibility labels for icons
|
||||
Add-this-ID = Add this identification
|
||||
@@ -1174,7 +1174,7 @@ The-exact-location-will-be-hidden = The exact location will be hidden publicly,
|
||||
The-location-will-not-be-visible = The location will not be visible to others, which means it may be difficult to identify.
|
||||
|
||||
# Wild status sheet descriptions
|
||||
This-is-a-wild-organism = This is a wild organism and wasn’t placed in this location by humans.
|
||||
This-is-a-wild-organism = This is a wild organism and wasn't placed in this location by humans.
|
||||
This-organism-was-placed-by-humans = This organism was placed in this location by humans. This applies to things like garden plants, pets, and zoo animals.
|
||||
|
||||
# Latitude, longitude, and accuracy on a single line on a single line
|
||||
@@ -1185,7 +1185,7 @@ Lat-Lon = { NUMBER($latitude, maximumFractionDigits: 6) }, { NUMBER($longitude,
|
||||
|
||||
|
||||
# Missing evidence sheet
|
||||
Every-observation-needs = Every observation needs a location, date, and time to be helpful to identifiers. You can edit geoprivacy if you’re concerned about location privacy.
|
||||
Every-observation-needs = Every observation needs a location, date, and time to be helpful to identifiers. You can edit geoprivacy if you're concerned about location privacy.
|
||||
# Button or accessibility label for an interactive element that stops an upload
|
||||
Stop-upload = Stop upload
|
||||
|
||||
@@ -1255,7 +1255,7 @@ Please-click-the-link = Please click the link in the email within 60 minutes to
|
||||
|
||||
# Title for dialog telling the user that an Internet connection is required
|
||||
Internet-Connection-Required = Internet Connection Required
|
||||
Please-try-again-when-you-are-connected-to-the-internet=Please try again when you are connected to the Internet.
|
||||
Please-try-again-when-you-are-connected-to-the-internet = Please try again when you are connected to the Internet.
|
||||
|
||||
IDENTIFY = IDENTIFY
|
||||
PROJECTS = PROJECTS
|
||||
@@ -1269,7 +1269,7 @@ Log-in-to-iNaturalist = Log in to iNaturalist
|
||||
|
||||
Try-searching-for-a-location-name = Try searching for a location name to see the map
|
||||
Scan-the-area-around-you-for-organisms = Scan the area around you for organisms.
|
||||
Loading-iNaturalists-AR-Camera = Loading iNaturalist’s AR Camera
|
||||
Loading-iNaturalists-AR-Camera = Loading iNaturalist's AR Camera
|
||||
|
||||
# Used for explore screen when search params lead to a search with no data
|
||||
No-results-found = No results found
|
||||
@@ -1277,7 +1277,7 @@ No-results-found = No results found
|
||||
# Default accessibility label for DisplayTaxon component
|
||||
Taxon-photo-and-name = Taxon photo and name
|
||||
|
||||
You-havent-joined-any-projects-yet = You haven’t joined any projects yet!
|
||||
You-havent-joined-any-projects-yet = You haven't joined any projects yet!
|
||||
|
||||
You-can-click-join-on-the-project-page = You can click “join” on the project page.
|
||||
|
||||
@@ -1299,9 +1299,9 @@ ABOUT-COLLECTION-PROJECTS = ABOUT COLLECTION PROJECTS
|
||||
ABOUT-TRADITIONAL-PROJECTS = ABOUT TRADITIONAL PROJECTS
|
||||
ABOUT-UMBRELLA-PROJECTS = ABOUT UMBRELLA PROJECTS
|
||||
|
||||
Every-time-a-collection-project = Every time a collection project’s page is loaded, iNaturalist will perform a quick search and display all observations that match the project’s requirements. It is an easy way to display a set of observations, such as for a class project, a park, or a bioblitz without making participants take the extra step of manually adding their observations to a project.
|
||||
Every-time-a-collection-project = Every time a collection project's page is loaded, iNaturalist will perform a quick search and display all observations that match the project's requirements. It is an easy way to display a set of observations, such as for a class project, a park, or a bioblitz without making participants take the extra step of manually adding their observations to a project.
|
||||
Obervations-must-be-manually-added = Observations must be manually added to a traditional project, either during the upload stage or after the observation has been shared to iNaturalist. A user must also join a traditional project in order to add their observations to it.
|
||||
If-you-want-to-collate-compare-promote = If you want to collate, compare, or promote a set of existing projects, then an Umbrella project is what you should use. For example the 2018 City Nature Challenge, which collated over 60 projects, made for a great landing page where anyone could compare and contrast each city’s observations. Both Collection and Traditional projects can be used in an Umbrella project, and up to 500 projects can be collated by an Umbrella project.
|
||||
If-you-want-to-collate-compare-promote = If you want to collate, compare, or promote a set of existing projects, then an Umbrella project is what you should use. For example the 2018 City Nature Challenge, which collated over 60 projects, made for a great landing page where anyone could compare and contrast each city's observations. Both Collection and Traditional projects can be used in an Umbrella project, and up to 500 projects can be collated by an Umbrella project.
|
||||
|
||||
JOIN-PROJECT = JOIN PROJECT
|
||||
JOIN = JOIN
|
||||
@@ -1309,3 +1309,17 @@ JOIN = JOIN
|
||||
LEAVE-PROJECT = LEAVE PROJECT
|
||||
LEAVE = LEAVE
|
||||
Your-email-is-confirmed = Your email is confirmed! Please log in to continue.
|
||||
|
||||
SEARCH-FOR-A-TAXON = SEARCH FOR A TAXON
|
||||
Select-the-identification-you-want-to-add = Select the identification you want to add to this observation. You can add a filter to further refine your results or search for a taxon.
|
||||
TOP-ID-SUGGESTION = TOP ID SUGGESTION
|
||||
NEARBY-SUGGESTIONS = NEARBY SUGGESTIONS
|
||||
INCLUDE-TAXA-NOT-EXPECTED-NEARBY = INCLUDE TAXA NOT EXPECTED NEARBY
|
||||
ONLY-SHOW-TAXA-EXPECTED-NEARBY = ONLY-SHOW-TAXA-EXPECTED-NEARBY
|
||||
iNaturalist-Identification-suggestions-are-trained-on = iNaturalist's Identification suggestions are trained on observations and identifications made by the iNaturalist community, including {$user1}, {$user2}, {$user3}, and many others.
|
||||
SPECIES-NEARBY = SPECIES NEARBY
|
||||
Below-are-all-the-species-observed-within-50km = Below are all the species observed within 50 km of your location within the taxon:
|
||||
Species-Nearby-requires-internet-to-work = Species Nearby requires internet to work. Please check your internet connection.
|
||||
Your-identification-will-be-posted-with-the-following-comment = Your identification will be posted with the following comment:
|
||||
iNaturalist-isnt-able-to-provide-a-top-ID-suggestion-for-this-photo = iNaturalist isn't able to provide a top ID suggestion for this photo.
|
||||
iNaturalist-has-no-ID-suggestions-for-this-photo = iNaturalist has no ID suggestions for this photo.
|
||||
@@ -204,7 +204,7 @@
|
||||
},
|
||||
"IDENTIFICATION": "IDENTIFICATION",
|
||||
"IDENTIFICATIONS": "IDENTIFICATIONS",
|
||||
"If-an-account-with-that-email-exists": "If an account with that email exists, we’ve sent password reset instructions to your email.",
|
||||
"If-an-account-with-that-email-exists": "If an account with that email exists, we've sent password reset instructions to your email.",
|
||||
"Import-X-photos": {
|
||||
"comment": "Shows the number of photos a user selected from the camera roll for upload",
|
||||
"val": "Import { $count ->\n [one] 1 photo\n *[other] { $count } photos\n}"
|
||||
@@ -430,7 +430,6 @@
|
||||
"Search-for-a-location": "Search for a location",
|
||||
"Search-for-a-project": "Search for a project",
|
||||
"Search-for-a-taxon": "Search for a taxon",
|
||||
"Search-for-a-taxon-to-add-an-identification": "Search for a taxon to add an identification.",
|
||||
"Search-for-a-user": "Search for a user",
|
||||
"Search-for-description-tags-text": "Search for description/tags text",
|
||||
"Select": "Select",
|
||||
@@ -519,7 +518,7 @@
|
||||
},
|
||||
"VIEW-DATA-QUALITY-ASSESSEMENT": "VIEW DATA QUALITY ASSESSEMENT",
|
||||
"View-in-browser": "View in Browser",
|
||||
"Visually-search-iNaturalist-data": "Visually search iNaturalist’s wealth of data. Search by a taxon in a location",
|
||||
"Visually-search-iNaturalist-data": "Visually search iNaturalist's wealth of data. Search by a taxon in a location",
|
||||
"Welcome-to-iNaturalist": "Welcome to iNaturalist!",
|
||||
"Whenever-you-get-internet-connection-you-can-upload": "Whenever you get internet connection, you can upload your observations to iNaturalist.",
|
||||
"Which-traditional-projects-can-add-your-observations": "Which traditional projects can add your observations?",
|
||||
@@ -556,7 +555,7 @@
|
||||
"Take-a-photo-with-your-camera": "Take a photo with your camera",
|
||||
"Upload-a-photo-from-your-gallery": "Upload a photo from your gallery",
|
||||
"Record-a-sound": "Record a sound",
|
||||
"You-can-also-explore-existing-observations": "You can also explore existing observations on iNaturalist to discover what’s around you.",
|
||||
"You-can-also-explore-existing-observations": "You can also explore existing observations on iNaturalist to discover what's around you.",
|
||||
"You-denied-iNaturalist-permission-to-do-that": {
|
||||
"comment": "Message shown when a permission is required to use a part of the app\n(e.g. permission to access the camera) but the user denied the permission.",
|
||||
"val": "You denied iNaturalist permission to do that"
|
||||
@@ -565,7 +564,7 @@
|
||||
"You-must-be-logged-in-to-view-messages": "You must be logged in to view messages",
|
||||
"You-will-lose-all-existing-observations": "{ $count ->\n [one] You will lose all existing observations. Would you like to discard 1 observation?\n *[other] You will lose all existing observations. Would you like to discard { $count } observations?\n}",
|
||||
"You-can-still-share-the-file": "You can still share the file with another app. If you can email it, please send it to { $email }",
|
||||
"Zoom-in": "Zoom in so that the observation’s accuracy is as low as possible.",
|
||||
"Zoom-in": "Zoom in so that the observation's accuracy is as low as possible.",
|
||||
"Your-location-uncertainty-is-over-4000km": "Your location uncertainty is over 4000km, which is too high to be helpful to identifiers. Edit the location and zoom in until the accuracy circle turns green and is centered on where you observed the organism.",
|
||||
"Category-leading": {
|
||||
"comment": "Identification category",
|
||||
@@ -711,6 +710,8 @@
|
||||
"Observation-photos-unavailable-without-internet": "Observation photos unavailable without internet",
|
||||
"Taxon-photo-unavailable-without-internet": "Taxon photo unavailable without internet",
|
||||
"User-photo-unavailable-without-internet": "User photo unavailable without internet",
|
||||
"Search": "Search",
|
||||
"Select-photo": "Select photo",
|
||||
"Add-this-ID": {
|
||||
"comment": "Accessibility labels for icons",
|
||||
"val": "Add this identification"
|
||||
@@ -800,7 +801,7 @@
|
||||
"The-location-will-not-be-visible": "The location will not be visible to others, which means it may be difficult to identify.",
|
||||
"This-is-a-wild-organism": {
|
||||
"comment": " Wild status sheet descriptions",
|
||||
"val": "This is a wild organism and wasn’t placed in this location by humans."
|
||||
"val": "This is a wild organism and wasn't placed in this location by humans."
|
||||
},
|
||||
"This-organism-was-placed-by-humans": "This organism was placed in this location by humans. This applies to things like garden plants, pets, and zoo animals.",
|
||||
"Lat-Lon-Acc": {
|
||||
@@ -813,7 +814,7 @@
|
||||
},
|
||||
"Every-observation-needs": {
|
||||
"comment": "Missing evidence sheet",
|
||||
"val": "Every observation needs a location, date, and time to be helpful to identifiers. You can edit geoprivacy if you’re concerned about location privacy."
|
||||
"val": "Every observation needs a location, date, and time to be helpful to identifiers. You can edit geoprivacy if you're concerned about location privacy."
|
||||
},
|
||||
"Stop-upload": {
|
||||
"comment": "Button or accessibility label for an interactive element that stops an upload",
|
||||
@@ -878,7 +879,7 @@
|
||||
"Log-in-to-iNaturalist": "Log in to iNaturalist",
|
||||
"Try-searching-for-a-location-name": "Try searching for a location name to see the map",
|
||||
"Scan-the-area-around-you-for-organisms": "Scan the area around you for organisms.",
|
||||
"Loading-iNaturalists-AR-Camera": "Loading iNaturalist’s AR Camera",
|
||||
"Loading-iNaturalists-AR-Camera": "Loading iNaturalist's AR Camera",
|
||||
"No-results-found": {
|
||||
"comment": "Used for explore screen when search params lead to a search with no data",
|
||||
"val": "No results found"
|
||||
@@ -887,7 +888,7 @@
|
||||
"comment": "Default accessibility label for DisplayTaxon component",
|
||||
"val": "Taxon photo and name"
|
||||
},
|
||||
"You-havent-joined-any-projects-yet": "You haven’t joined any projects yet!",
|
||||
"You-havent-joined-any-projects-yet": "You haven't joined any projects yet!",
|
||||
"You-can-click-join-on-the-project-page": "You can click “join” on the project page.",
|
||||
"No-projects-match-that-search": "No projects match that search",
|
||||
"RESET-SEARCH": "RESET SEARCH",
|
||||
@@ -900,12 +901,25 @@
|
||||
"ABOUT-COLLECTION-PROJECTS": "ABOUT COLLECTION PROJECTS",
|
||||
"ABOUT-TRADITIONAL-PROJECTS": "ABOUT TRADITIONAL PROJECTS",
|
||||
"ABOUT-UMBRELLA-PROJECTS": "ABOUT UMBRELLA PROJECTS",
|
||||
"Every-time-a-collection-project": "Every time a collection project’s page is loaded, iNaturalist will perform a quick search and display all observations that match the project’s requirements. It is an easy way to display a set of observations, such as for a class project, a park, or a bioblitz without making participants take the extra step of manually adding their observations to a project.",
|
||||
"Every-time-a-collection-project": "Every time a collection project's page is loaded, iNaturalist will perform a quick search and display all observations that match the project's requirements. It is an easy way to display a set of observations, such as for a class project, a park, or a bioblitz without making participants take the extra step of manually adding their observations to a project.",
|
||||
"Obervations-must-be-manually-added": "Observations must be manually added to a traditional project, either during the upload stage or after the observation has been shared to iNaturalist. A user must also join a traditional project in order to add their observations to it.",
|
||||
"If-you-want-to-collate-compare-promote": "If you want to collate, compare, or promote a set of existing projects, then an Umbrella project is what you should use. For example the 2018 City Nature Challenge, which collated over 60 projects, made for a great landing page where anyone could compare and contrast each city’s observations. Both Collection and Traditional projects can be used in an Umbrella project, and up to 500 projects can be collated by an Umbrella project.",
|
||||
"If-you-want-to-collate-compare-promote": "If you want to collate, compare, or promote a set of existing projects, then an Umbrella project is what you should use. For example the 2018 City Nature Challenge, which collated over 60 projects, made for a great landing page where anyone could compare and contrast each city's observations. Both Collection and Traditional projects can be used in an Umbrella project, and up to 500 projects can be collated by an Umbrella project.",
|
||||
"JOIN-PROJECT": "JOIN PROJECT",
|
||||
"JOIN": "JOIN",
|
||||
"LEAVE-PROJECT": "LEAVE PROJECT",
|
||||
"LEAVE": "LEAVE",
|
||||
"Your-email-is-confirmed": "Your email is confirmed! Please log in to continue."
|
||||
"Your-email-is-confirmed": "Your email is confirmed! Please log in to continue.",
|
||||
"SEARCH-FOR-A-TAXON": "SEARCH FOR A TAXON",
|
||||
"Select-the-identification-you-want-to-add": "Select the identification you want to add to this observation. You can add a filter to further refine your results or search for a taxon.",
|
||||
"TOP-ID-SUGGESTION": "TOP ID SUGGESTION",
|
||||
"NEARBY-SUGGESTIONS": "NEARBY SUGGESTIONS",
|
||||
"INCLUDE-TAXA-NOT-EXPECTED-NEARBY": "INCLUDE TAXA NOT EXPECTED NEARBY",
|
||||
"ONLY-SHOW-TAXA-EXPECTED-NEARBY": "ONLY-SHOW-TAXA-EXPECTED-NEARBY",
|
||||
"iNaturalist-Identification-suggestions-are-trained-on": "iNaturalist's Identification suggestions are trained on observations and identifications made by the iNaturalist community, including { $user1 }, { $user2 }, { $user3 }, and many others.",
|
||||
"SPECIES-NEARBY": "SPECIES NEARBY",
|
||||
"Below-are-all-the-species-observed-within-50km": "Below are all the species observed within 50 km of your location within the taxon:",
|
||||
"Species-Nearby-requires-internet-to-work": "Species Nearby requires internet to work. Please check your internet connection.",
|
||||
"Your-identification-will-be-posted-with-the-following-comment": "Your identification will be posted with the following comment:",
|
||||
"iNaturalist-isnt-able-to-provide-a-top-ID-suggestion-for-this-photo": "iNaturalist isn't able to provide a top ID suggestion for this photo.",
|
||||
"iNaturalist-has-no-ID-suggestions-for-this-photo": "iNaturalist has no ID suggestions for this photo."
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ IDENTIFICATION = IDENTIFICATION
|
||||
|
||||
IDENTIFICATIONS = IDENTIFICATIONS
|
||||
|
||||
If-an-account-with-that-email-exists = If an account with that email exists, we’ve sent password reset instructions to your email.
|
||||
If-an-account-with-that-email-exists = If an account with that email exists, we've sent password reset instructions to your email.
|
||||
|
||||
# Shows the number of photos a user selected from the camera roll for upload
|
||||
Import-X-photos = Import {$count ->
|
||||
@@ -704,8 +704,6 @@ Search-for-a-project = Search for a project
|
||||
|
||||
Search-for-a-taxon = Search for a taxon
|
||||
|
||||
Search-for-a-taxon-to-add-an-identification = Search for a taxon to add an identification.
|
||||
|
||||
Search-for-a-user = Search for a user
|
||||
|
||||
Search-for-description-tags-text = Search for description/tags text
|
||||
@@ -846,7 +844,7 @@ VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
|
||||
|
||||
View-in-browser = View in Browser
|
||||
|
||||
Visually-search-iNaturalist-data = Visually search iNaturalist’s wealth of data. Search by a taxon in a location
|
||||
Visually-search-iNaturalist-data = Visually search iNaturalist's wealth of data. Search by a taxon in a location
|
||||
|
||||
Welcome-to-iNaturalist = Welcome to iNaturalist!
|
||||
|
||||
@@ -916,7 +914,7 @@ Take-a-photo-with-your-camera = Take a photo with your camera
|
||||
Upload-a-photo-from-your-gallery = Upload a photo from your gallery
|
||||
Record-a-sound = Record a sound
|
||||
|
||||
You-can-also-explore-existing-observations = You can also explore existing observations on iNaturalist to discover what’s around you.
|
||||
You-can-also-explore-existing-observations = You can also explore existing observations on iNaturalist to discover what's around you.
|
||||
|
||||
# Message shown when a permission is required to use a part of the app
|
||||
# (e.g. permission to access the camera) but the user denied the permission.
|
||||
@@ -934,7 +932,7 @@ You-will-lose-all-existing-observations = {$count ->
|
||||
You-can-still-share-the-file =
|
||||
You can still share the file with another app. If you can email it, please send it to { $email }
|
||||
|
||||
Zoom-in = Zoom in so that the observation’s accuracy is as low as possible.
|
||||
Zoom-in = Zoom in so that the observation's accuracy is as low as possible.
|
||||
|
||||
Your-location-uncertainty-is-over-4000km = Your location uncertainty is over 4000km, which is too high to be helpful to identifiers. Edit the location and zoom in until the accuracy circle turns green and is centered on where you observed the organism.
|
||||
|
||||
@@ -1085,6 +1083,8 @@ Location-map-unavailable-without-internet = Location map unavailable without int
|
||||
Observation-photos-unavailable-without-internet = Observation photos unavailable without internet
|
||||
Taxon-photo-unavailable-without-internet = Taxon photo unavailable without internet
|
||||
User-photo-unavailable-without-internet = User photo unavailable without internet
|
||||
Search = Search
|
||||
Select-photo = Select photo
|
||||
|
||||
# Accessibility labels for icons
|
||||
Add-this-ID = Add this identification
|
||||
@@ -1174,7 +1174,7 @@ The-exact-location-will-be-hidden = The exact location will be hidden publicly,
|
||||
The-location-will-not-be-visible = The location will not be visible to others, which means it may be difficult to identify.
|
||||
|
||||
# Wild status sheet descriptions
|
||||
This-is-a-wild-organism = This is a wild organism and wasn’t placed in this location by humans.
|
||||
This-is-a-wild-organism = This is a wild organism and wasn't placed in this location by humans.
|
||||
This-organism-was-placed-by-humans = This organism was placed in this location by humans. This applies to things like garden plants, pets, and zoo animals.
|
||||
|
||||
# Latitude, longitude, and accuracy on a single line on a single line
|
||||
@@ -1185,7 +1185,7 @@ Lat-Lon = { NUMBER($latitude, maximumFractionDigits: 6) }, { NUMBER($longitude,
|
||||
|
||||
|
||||
# Missing evidence sheet
|
||||
Every-observation-needs = Every observation needs a location, date, and time to be helpful to identifiers. You can edit geoprivacy if you’re concerned about location privacy.
|
||||
Every-observation-needs = Every observation needs a location, date, and time to be helpful to identifiers. You can edit geoprivacy if you're concerned about location privacy.
|
||||
# Button or accessibility label for an interactive element that stops an upload
|
||||
Stop-upload = Stop upload
|
||||
|
||||
@@ -1255,7 +1255,7 @@ Please-click-the-link = Please click the link in the email within 60 minutes to
|
||||
|
||||
# Title for dialog telling the user that an Internet connection is required
|
||||
Internet-Connection-Required = Internet Connection Required
|
||||
Please-try-again-when-you-are-connected-to-the-internet=Please try again when you are connected to the Internet.
|
||||
Please-try-again-when-you-are-connected-to-the-internet = Please try again when you are connected to the Internet.
|
||||
|
||||
IDENTIFY = IDENTIFY
|
||||
PROJECTS = PROJECTS
|
||||
@@ -1269,7 +1269,7 @@ Log-in-to-iNaturalist = Log in to iNaturalist
|
||||
|
||||
Try-searching-for-a-location-name = Try searching for a location name to see the map
|
||||
Scan-the-area-around-you-for-organisms = Scan the area around you for organisms.
|
||||
Loading-iNaturalists-AR-Camera = Loading iNaturalist’s AR Camera
|
||||
Loading-iNaturalists-AR-Camera = Loading iNaturalist's AR Camera
|
||||
|
||||
# Used for explore screen when search params lead to a search with no data
|
||||
No-results-found = No results found
|
||||
@@ -1277,7 +1277,7 @@ No-results-found = No results found
|
||||
# Default accessibility label for DisplayTaxon component
|
||||
Taxon-photo-and-name = Taxon photo and name
|
||||
|
||||
You-havent-joined-any-projects-yet = You haven’t joined any projects yet!
|
||||
You-havent-joined-any-projects-yet = You haven't joined any projects yet!
|
||||
|
||||
You-can-click-join-on-the-project-page = You can click “join” on the project page.
|
||||
|
||||
@@ -1299,9 +1299,9 @@ ABOUT-COLLECTION-PROJECTS = ABOUT COLLECTION PROJECTS
|
||||
ABOUT-TRADITIONAL-PROJECTS = ABOUT TRADITIONAL PROJECTS
|
||||
ABOUT-UMBRELLA-PROJECTS = ABOUT UMBRELLA PROJECTS
|
||||
|
||||
Every-time-a-collection-project = Every time a collection project’s page is loaded, iNaturalist will perform a quick search and display all observations that match the project’s requirements. It is an easy way to display a set of observations, such as for a class project, a park, or a bioblitz without making participants take the extra step of manually adding their observations to a project.
|
||||
Every-time-a-collection-project = Every time a collection project's page is loaded, iNaturalist will perform a quick search and display all observations that match the project's requirements. It is an easy way to display a set of observations, such as for a class project, a park, or a bioblitz without making participants take the extra step of manually adding their observations to a project.
|
||||
Obervations-must-be-manually-added = Observations must be manually added to a traditional project, either during the upload stage or after the observation has been shared to iNaturalist. A user must also join a traditional project in order to add their observations to it.
|
||||
If-you-want-to-collate-compare-promote = If you want to collate, compare, or promote a set of existing projects, then an Umbrella project is what you should use. For example the 2018 City Nature Challenge, which collated over 60 projects, made for a great landing page where anyone could compare and contrast each city’s observations. Both Collection and Traditional projects can be used in an Umbrella project, and up to 500 projects can be collated by an Umbrella project.
|
||||
If-you-want-to-collate-compare-promote = If you want to collate, compare, or promote a set of existing projects, then an Umbrella project is what you should use. For example the 2018 City Nature Challenge, which collated over 60 projects, made for a great landing page where anyone could compare and contrast each city's observations. Both Collection and Traditional projects can be used in an Umbrella project, and up to 500 projects can be collated by an Umbrella project.
|
||||
|
||||
JOIN-PROJECT = JOIN PROJECT
|
||||
JOIN = JOIN
|
||||
@@ -1309,3 +1309,17 @@ JOIN = JOIN
|
||||
LEAVE-PROJECT = LEAVE PROJECT
|
||||
LEAVE = LEAVE
|
||||
Your-email-is-confirmed = Your email is confirmed! Please log in to continue.
|
||||
|
||||
SEARCH-FOR-A-TAXON = SEARCH FOR A TAXON
|
||||
Select-the-identification-you-want-to-add = Select the identification you want to add to this observation. You can add a filter to further refine your results or search for a taxon.
|
||||
TOP-ID-SUGGESTION = TOP ID SUGGESTION
|
||||
NEARBY-SUGGESTIONS = NEARBY SUGGESTIONS
|
||||
INCLUDE-TAXA-NOT-EXPECTED-NEARBY = INCLUDE TAXA NOT EXPECTED NEARBY
|
||||
ONLY-SHOW-TAXA-EXPECTED-NEARBY = ONLY-SHOW-TAXA-EXPECTED-NEARBY
|
||||
iNaturalist-Identification-suggestions-are-trained-on = iNaturalist's Identification suggestions are trained on observations and identifications made by the iNaturalist community, including {$user1}, {$user2}, {$user3}, and many others.
|
||||
SPECIES-NEARBY = SPECIES NEARBY
|
||||
Below-are-all-the-species-observed-within-50km = Below are all the species observed within 50 km of your location within the taxon:
|
||||
Species-Nearby-requires-internet-to-work = Species Nearby requires internet to work. Please check your internet connection.
|
||||
Your-identification-will-be-posted-with-the-following-comment = Your identification will be posted with the following comment:
|
||||
iNaturalist-isnt-able-to-provide-a-top-ID-suggestion-for-this-photo = iNaturalist isn't able to provide a top ID suggestion for this photo.
|
||||
iNaturalist-has-no-ID-suggestions-for-this-photo = iNaturalist has no ID suggestions for this photo.
|
||||
7
src/images/icons/edit-comment.svg
Normal file
7
src/images/icons/edit-comment.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg width="24" height="24" viewBox="0 0 25 25">
|
||||
<path d="M12.5003 2C6.56731 2 2.04199 6.06439 2.04199 10.7544C2.04199 13.4124 3.4668 15.8391 5.80285 17.4728L6.34089 17.8491L6.20952 18.4924C5.90552 19.9811 5.09007 21.4712 3.85494 22.9611C5.73527 22.7587 7.47993 21.7172 9.00129 19.6458L9.38053 19.1295L10.0081 19.2582C10.8057 19.4217 11.6402 19.5087 12.5003 19.5087C18.4333 19.5087 22.9587 15.4443 22.9587 10.7544C22.9587 6.06439 18.4333 2 12.5003 2ZM0.0419922 10.7544C0.0419922 4.66997 5.77681 0 12.5003 0C19.2238 0 24.9587 4.66997 24.9587 10.7544C24.9587 16.8388 19.2238 21.5087 12.5003 21.5087C11.725 21.5087 10.9654 21.4476 10.2278 21.3304C8.3709 23.6305 6.08704 24.8912 3.49298 24.9917C2.88211 25.0154 2.34569 24.9948 1.94297 24.8613C1.73617 24.7928 1.42873 24.651 1.21604 24.3366C0.974446 23.9794 0.983259 23.5967 1.05807 23.3258C1.12439 23.0857 1.24887 22.8961 1.33463 22.7792C1.42786 22.6521 1.53299 22.5349 1.62279 22.4392C1.71875 22.337 1.79359 22.2613 1.85902 22.195C1.94512 22.1078 2.01492 22.0371 2.0951 21.9445C3.1235 20.7564 3.76747 19.6721 4.09001 18.6906C1.63049 16.7484 0.0419922 13.9318 0.0419922 10.7544Z"/>
|
||||
<circle cx="8.1" cy="11" r="1.1"/>
|
||||
<circle cx="12.5004" cy="11" r="1.1"/>
|
||||
<circle cx="16.8998" cy="11" r="1.1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -8,7 +8,6 @@ import MyObservationsContainer from "components/MyObservations/MyObservationsCon
|
||||
import DataQualityAssessment from "components/ObsDetails/DataQualityAssessment";
|
||||
import ObsDetailsContainer from "components/ObsDetails/ObsDetailsContainer";
|
||||
import PlaceholderComponent from "components/PlaceholderComponent";
|
||||
import TaxonDetails from "components/TaxonDetails/TaxonDetails";
|
||||
import UserProfile from "components/UserProfile/UserProfile";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
@@ -49,11 +48,6 @@ const ObservationsStackNavigator = ( ): Node => (
|
||||
unmountOnBlur: true
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="TaxonDetails"
|
||||
component={TaxonDetails}
|
||||
options={hideHeader}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="UserProfile"
|
||||
component={UserProfile}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import AddIDContainer from "components/AddID/AddIDContainer";
|
||||
import LocationPickerContainer from "components/LocationPicker/LocationPickerContainer";
|
||||
import MediaViewer from "components/MediaViewer/MediaViewer";
|
||||
import ObsEdit from "components/ObsEdit/ObsEdit";
|
||||
import { Heading4, Mortal, PermissionGate } from "components/SharedComponents";
|
||||
import SuggestionsContainer from "components/Suggestions/SuggestionsContainer";
|
||||
import TaxonSearch from "components/Suggestions/TaxonSearch";
|
||||
import TaxonDetails from "components/TaxonDetails/TaxonDetails";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
blankHeaderTitle,
|
||||
@@ -17,7 +19,8 @@ import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { PermissionsAndroid } from "react-native";
|
||||
|
||||
const addIDTitle = ( ) => <Heading4>{t( "ADD-AN-ID" )}</Heading4>;
|
||||
const suggestionsTitle = ( ) => <Heading4>{t( "ADD-AN-ID" )}</Heading4>;
|
||||
const taxonSearchTitle = ( ) => <Heading4>{t( "SEARCH" )}</Heading4>;
|
||||
|
||||
const Stack = createNativeStackNavigator( );
|
||||
|
||||
@@ -32,7 +35,14 @@ const ObsEditWithPermission = ( ) => (
|
||||
);
|
||||
|
||||
const SharedStackScreens = ( ): Node => (
|
||||
<Stack.Group>
|
||||
<Stack.Group
|
||||
screenOptions={{
|
||||
cardStyle: {
|
||||
backgroundColor: "rgba(0,0,0,0)",
|
||||
opacity: 1
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="ObsEdit"
|
||||
component={ObsEditWithPermission}
|
||||
@@ -44,11 +54,21 @@ const SharedStackScreens = ( ): Node => (
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AddID"
|
||||
component={AddIDContainer}
|
||||
name="Suggestions"
|
||||
component={SuggestionsContainer}
|
||||
options={{
|
||||
...removeBottomBorder,
|
||||
headerTitle: addIDTitle,
|
||||
headerTitle: suggestionsTitle,
|
||||
headerTitleAlign: "center",
|
||||
headerBackTitleVisible: false
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="TaxonSearch"
|
||||
component={TaxonSearch}
|
||||
options={{
|
||||
...removeBottomBorder,
|
||||
headerTitle: taxonSearchTitle,
|
||||
headerTitleAlign: "center"
|
||||
}}
|
||||
/>
|
||||
@@ -70,6 +90,11 @@ const SharedStackScreens = ( ): Node => (
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="TaxonDetails"
|
||||
component={TaxonDetails}
|
||||
options={hideHeader}
|
||||
/>
|
||||
</Stack.Group>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { activateKeepAwake, deactivateKeepAwake } from "@sayem314/react-native-keep-awake";
|
||||
import { createIdentification } from "api/identifications";
|
||||
import {
|
||||
createObservation,
|
||||
createOrUpdateEvidence,
|
||||
@@ -17,7 +18,9 @@ import React, {
|
||||
useReducer,
|
||||
useState
|
||||
} from "react";
|
||||
import { Alert } from "react-native";
|
||||
import { EventRegister } from "react-native-event-listeners";
|
||||
import rnUUID from "react-native-uuid";
|
||||
import Observation from "realmModels/Observation";
|
||||
import ObservationPhoto from "realmModels/ObservationPhoto";
|
||||
import Photo from "realmModels/Photo";
|
||||
@@ -28,7 +31,10 @@ import fetchPlaceName from "sharedHelpers/fetchPlaceName";
|
||||
import { formatExifDateAsString, parseExif, writeExifToFile } from "sharedHelpers/parseExif";
|
||||
import {
|
||||
useApiToken,
|
||||
useCurrentUser
|
||||
useAuthenticatedMutation,
|
||||
useCurrentUser,
|
||||
useLocalObservation,
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
|
||||
import { log } from "../../react-native-logs.config";
|
||||
@@ -118,6 +124,7 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
const realm = useRealm( );
|
||||
const apiToken = useApiToken( );
|
||||
const currentUser = useCurrentUser( );
|
||||
const { t } = useTranslation( );
|
||||
// state related to creating/editing an observation
|
||||
const [currentObservationIndex, setCurrentObservationIndex] = useState( 0 );
|
||||
const [observations, setObservations] = useState( [] );
|
||||
@@ -131,10 +138,12 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
const [unsavedChanges, setUnsavedChanges] = useState( false );
|
||||
const [passesEvidenceTest, setPassesEvidenceTest] = useState( false );
|
||||
const [passesIdentificationTest, setPassesIdentificationTest] = useState( false );
|
||||
const [mediaViewerUris, setMediaViewerUris] = useState( [] );
|
||||
const [photoEvidenceUris, setPhotoEvidenceUris] = useState( [] );
|
||||
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState( 0 );
|
||||
const [groupedPhotos, setGroupedPhotos] = useState( [] );
|
||||
const [savingPhoto, setSavingPhoto] = useState( false );
|
||||
const [lastScreen, setLastScreen] = useState( "" );
|
||||
const [comment, setComment] = useState( "" );
|
||||
|
||||
// state related to uploads in useReducer
|
||||
const [state, dispatch] = useReducer( reducer, initialState );
|
||||
@@ -168,6 +177,8 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setUnsavedChanges( false );
|
||||
setPassesEvidenceTest( false );
|
||||
setGroupedPhotos( [] );
|
||||
setPhotoEvidenceUris( [] );
|
||||
setComment( "" );
|
||||
}, [] );
|
||||
|
||||
const stopUpload = ( ) => {
|
||||
@@ -216,6 +227,9 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
const currentObservation = observations[currentObservationIndex];
|
||||
const numOfObsPhotos = currentObservation?.observationPhotos?.length || 0;
|
||||
|
||||
const localObservation = useLocalObservation( currentObservation?.uuid );
|
||||
const wasSynced = localObservation?.wasSynced( );
|
||||
|
||||
const addSound = async ( ) => {
|
||||
const newObservation = await Observation.createObsWithSounds( );
|
||||
setObservations( [newObservation] );
|
||||
@@ -303,6 +317,46 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setSavingPhoto( false );
|
||||
}, [createObsPhotos, appendObsPhotos, numOfObsPhotos] );
|
||||
|
||||
const createIdentificationMutation = useAuthenticatedMutation(
|
||||
( idParams, optsWithAuth ) => createIdentification( idParams, optsWithAuth ),
|
||||
{
|
||||
onSuccess: data => {
|
||||
const belongsToCurrentUser = currentObservation?.user?.login === currentUser?.login;
|
||||
if ( belongsToCurrentUser && wasSynced ) {
|
||||
realm?.write( ( ) => {
|
||||
const localIdentifications = currentObservation?.identifications;
|
||||
const newIdentification = data[0];
|
||||
newIdentification.user = currentUser;
|
||||
newIdentification.taxon = realm?.objectForPrimaryKey(
|
||||
"Taxon",
|
||||
newIdentification.taxon.id
|
||||
) || newIdentification.taxon;
|
||||
const realmIdentification = realm?.create( "Identification", newIdentification );
|
||||
localIdentifications.push( realmIdentification );
|
||||
} );
|
||||
}
|
||||
setLoading( false );
|
||||
navigation.navigate( "ObservationsStackNavigator", {
|
||||
screen: "ObsDetails",
|
||||
params: { uuid: currentObservation.uuid }
|
||||
} );
|
||||
},
|
||||
onError: e => {
|
||||
const showErrorAlert = err => Alert.alert( "Error", err, [{ text: t( "OK" ) }], {
|
||||
cancelable: true
|
||||
} );
|
||||
let identificationError = null;
|
||||
if ( e ) {
|
||||
identificationError = t( "Couldnt-create-identification-error", { error: e.message } );
|
||||
} else {
|
||||
identificationError = t( "Couldnt-create-identification-unknown-error" );
|
||||
}
|
||||
setLoading( false );
|
||||
return showErrorAlert( identificationError );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const uploadValue = useMemo( ( ) => {
|
||||
// Save URIs to camera gallery (if a photo was taken using the app,
|
||||
// we want it accessible in the camera's folder, as if the user has taken those photos
|
||||
@@ -386,11 +440,49 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setObservations( [...updatedObservations] );
|
||||
};
|
||||
|
||||
const formatIdentification = taxon => {
|
||||
const newIdent = {
|
||||
uuid: rnUUID.v4(),
|
||||
body: comment,
|
||||
taxon
|
||||
};
|
||||
|
||||
return newIdent;
|
||||
};
|
||||
|
||||
const createId = identification => {
|
||||
setLoading( true );
|
||||
const newIdentification = formatIdentification( identification );
|
||||
const createRemoteIdentification = localObservation?.wasSynced( );
|
||||
if ( createRemoteIdentification ) {
|
||||
createIdentificationMutation.mutate( {
|
||||
identification: {
|
||||
observation_id: currentObservation.uuid,
|
||||
taxon_id: newIdentification.taxon.id,
|
||||
body: newIdentification.body
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
updateObservationKeys( {
|
||||
taxon: newIdentification.taxon
|
||||
} );
|
||||
setLoading( false );
|
||||
const navParams = {};
|
||||
if ( lastScreen === "ObsDetails" ) {
|
||||
navParams.uuid = currentObservation.uuid;
|
||||
}
|
||||
navigation.navigate( "ObservationsStackNavigator", {
|
||||
screen: lastScreen,
|
||||
params: navParams
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
const deleteLocalObservation = uuid => {
|
||||
const localObservation = realm.objectForPrimaryKey( "Observation", uuid );
|
||||
if ( !localObservation ) { return; }
|
||||
const localObsToDelete = realm.objectForPrimaryKey( "Observation", uuid );
|
||||
if ( !localObsToDelete ) { return; }
|
||||
realm?.write( ( ) => {
|
||||
realm?.delete( localObservation );
|
||||
realm?.delete( localObsToDelete );
|
||||
} );
|
||||
};
|
||||
|
||||
@@ -651,8 +743,8 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
}
|
||||
|
||||
// photos to show in media viewer
|
||||
const newMediaViewerUris = removePhotoFromList( mediaViewerUris, photoUriToDelete );
|
||||
setMediaViewerUris( [...newMediaViewerUris] );
|
||||
const newphotoEvidenceUris = removePhotoFromList( photoEvidenceUris, photoUriToDelete );
|
||||
setPhotoEvidenceUris( [...newphotoEvidenceUris] );
|
||||
|
||||
// photos displayed in PhotoPreview
|
||||
const newCameraPreviewUris = removePhotoFromList( cameraPreviewUris, photoUriToDelete );
|
||||
@@ -765,8 +857,8 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
passesEvidenceTest,
|
||||
passesIdentificationTest,
|
||||
setPassesIdentificationTest,
|
||||
mediaViewerUris,
|
||||
setMediaViewerUris,
|
||||
photoEvidenceUris,
|
||||
setPhotoEvidenceUris,
|
||||
selectedPhotoIndex,
|
||||
setSelectedPhotoIndex,
|
||||
groupedPhotos,
|
||||
@@ -783,7 +875,11 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setOriginalCameraUrisMap,
|
||||
savingPhoto,
|
||||
singleUpload,
|
||||
totalUploadCount
|
||||
totalUploadCount,
|
||||
createId,
|
||||
setLastScreen,
|
||||
comment,
|
||||
setComment
|
||||
};
|
||||
}, [
|
||||
currentObservation,
|
||||
@@ -810,7 +906,7 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
uploadProgress,
|
||||
passesEvidenceTest,
|
||||
passesIdentificationTest,
|
||||
mediaViewerUris,
|
||||
photoEvidenceUris,
|
||||
selectedPhotoIndex,
|
||||
groupedPhotos,
|
||||
currentUploadIndex,
|
||||
@@ -825,6 +921,10 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
savingPhoto,
|
||||
singleUpload,
|
||||
totalUploadCount,
|
||||
createIdentificationMutation,
|
||||
lastScreen,
|
||||
localObservation,
|
||||
comment,
|
||||
createObsPhotos,
|
||||
numOfObsPhotos
|
||||
] );
|
||||
|
||||
52
src/sharedHelpers/flattenUploadParams.js
Normal file
52
src/sharedHelpers/flattenUploadParams.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||
import { FileUpload } from "inaturalistjs";
|
||||
|
||||
const resizeImage = async (
|
||||
path: string,
|
||||
width: number,
|
||||
height?: number,
|
||||
outputPath?: string
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const { uri } = await ImageResizer.createResizedImage(
|
||||
path,
|
||||
width,
|
||||
height || width, // height
|
||||
"JPEG", // compressFormat
|
||||
100, // quality
|
||||
0, // rotation
|
||||
// $FlowFixMe
|
||||
outputPath, // outputPath
|
||||
true // keep metadata
|
||||
);
|
||||
|
||||
return uri;
|
||||
} catch ( e ) {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const flattenUploadParams = async (
|
||||
uri: string,
|
||||
latitude: number,
|
||||
longitude: number
|
||||
): Object => {
|
||||
const userImage = await resizeImage( uri, 299 );
|
||||
|
||||
const params = {
|
||||
image: new FileUpload( {
|
||||
uri: userImage,
|
||||
name: "photo.jpeg",
|
||||
type: "image/jpeg"
|
||||
} )
|
||||
};
|
||||
if ( latitude ) {
|
||||
params.latitude = latitude;
|
||||
}
|
||||
if ( longitude ) {
|
||||
params.longitude = longitude;
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
export default flattenUploadParams;
|
||||
@@ -44,7 +44,9 @@ const mockObsEditProviderWithObs = obs => ObsEditProvider.mockImplementation( (
|
||||
currentObservation: obs[0],
|
||||
updateObservationKeys: jest.fn( ),
|
||||
setPassesIdentificationTest: jest.fn( ),
|
||||
writeExifToCameraRollPhotos: jest.fn( )
|
||||
writeExifToCameraRollPhotos: jest.fn( ),
|
||||
photoEvidenceUris: [faker.image.imageUrl( )],
|
||||
setPhotoEvidenceUris: jest.fn( )
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -3,6 +3,9 @@ import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import ObsDetailsContainer from "components/ObsDetails/ObsDetailsContainer";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import { t } from "i18next";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import INatPaperProvider from "providers/INatPaperProvider";
|
||||
import ObsEditProvider from "providers/ObsEditProvider";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
@@ -11,6 +14,11 @@ import useIsConnected from "sharedHooks/useIsConnected";
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
// Mock ObservationProvider so it provides a specific array of observations
|
||||
// without any current observation or ability to update or fetch
|
||||
// observations
|
||||
jest.mock( "providers/ObsEditProvider" );
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
const mockObservation = factory( "LocalObservation", {
|
||||
_created_at: faker.date.past( ),
|
||||
@@ -150,13 +158,33 @@ jest.mock( "sharedHooks/useUserLocation", () => ( {
|
||||
default: () => ( { latLng: mockLatLng } )
|
||||
} ) );
|
||||
|
||||
const mockObsEditProviderWithObs = obs => ObsEditProvider.mockImplementation( ( { children } ) => (
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
<INatPaperProvider>
|
||||
<ObsEditContext.Provider value={{
|
||||
setPhotoEvidenceUris: jest.fn( ),
|
||||
observations: obs
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ObsEditContext.Provider>
|
||||
</INatPaperProvider>
|
||||
) );
|
||||
|
||||
const renderObsDetails = ( ) => renderComponent(
|
||||
<ObsEditProvider>
|
||||
<ObsDetailsContainer />
|
||||
</ObsEditProvider>
|
||||
);
|
||||
|
||||
describe( "ObsDetails", () => {
|
||||
beforeAll( async () => {
|
||||
await initI18next();
|
||||
} );
|
||||
|
||||
it( "should not have accessibility errors", async () => {
|
||||
renderComponent( <ObsDetailsContainer /> );
|
||||
mockObsEditProviderWithObs( [mockObservation] );
|
||||
renderObsDetails( );
|
||||
const obsDetails = await screen.findByTestId(
|
||||
`ObsDetails.${mockObservation.uuid}`
|
||||
);
|
||||
@@ -165,7 +193,8 @@ describe( "ObsDetails", () => {
|
||||
|
||||
it( "renders obs details from remote call", async () => {
|
||||
useIsConnected.mockImplementation( () => true );
|
||||
renderComponent( <ObsDetailsContainer /> );
|
||||
mockObsEditProviderWithObs( [mockObservation] );
|
||||
renderObsDetails( );
|
||||
|
||||
expect(
|
||||
await screen.findByTestId( `ObsDetails.${mockObservation.uuid}` )
|
||||
@@ -174,7 +203,8 @@ describe( "ObsDetails", () => {
|
||||
} );
|
||||
|
||||
it( "renders data tab on button press", async () => {
|
||||
renderComponent( <ObsDetailsContainer /> );
|
||||
mockObsEditProviderWithObs( [mockObservation] );
|
||||
renderObsDetails( );
|
||||
const button = await screen.findByTestId( "ObsDetails.DetailsTab" );
|
||||
expect( screen.queryByTestId( "mock-data-tab" ) ).not.toBeTruthy();
|
||||
|
||||
@@ -191,7 +221,8 @@ describe( "ObsDetails", () => {
|
||||
|
||||
it( "should render fallback image icon instead of photos", async () => {
|
||||
useIsConnected.mockImplementation( () => true );
|
||||
renderComponent( <ObsDetailsContainer /> );
|
||||
mockObsEditProviderWithObs( [mockObservation] );
|
||||
renderObsDetails( );
|
||||
|
||||
const labelText = t( "Observation-has-no-photos-and-no-sounds" );
|
||||
const fallbackImage = await screen.findByLabelText( labelText );
|
||||
@@ -207,7 +238,8 @@ describe( "ObsDetails", () => {
|
||||
|
||||
describe( "activity tab", () => {
|
||||
it( "navigates to taxon details on button press", async () => {
|
||||
renderComponent( <ObsDetailsContainer /> );
|
||||
mockObsEditProviderWithObs( [mockObservation] );
|
||||
renderObsDetails( );
|
||||
fireEvent.press(
|
||||
await screen.findByTestId(
|
||||
`ObsDetails.taxon.${mockObservation.taxon.id}`
|
||||
@@ -220,7 +252,8 @@ describe( "ObsDetails", () => {
|
||||
|
||||
it( "shows network error image instead of observation photos if user is offline", async () => {
|
||||
useIsConnected.mockImplementation( () => false );
|
||||
renderComponent( <ObsDetailsContainer /> );
|
||||
mockObsEditProviderWithObs( [mockObservation] );
|
||||
renderObsDetails( );
|
||||
const labelText = t( "Observation-photos-unavailable-without-internet" );
|
||||
const noInternet = await screen.findByLabelText( labelText );
|
||||
expect( noInternet ).toBeTruthy();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { screen } from "@testing-library/react-native";
|
||||
import EvidenceList from "components/ObsEdit/EvidenceList";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import INatPaperProvider from "providers/INatPaperProvider";
|
||||
import ObsEditProvider from "providers/ObsEditProvider";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
@@ -21,45 +24,52 @@ const observationPhotos = [
|
||||
} )
|
||||
];
|
||||
|
||||
jest.mock( "providers/ObsEditProvider" );
|
||||
const mockObsEditProvider = ( savingPhoto = false ) => ObsEditProvider
|
||||
.mockImplementation( ( { children } ) => (
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
<INatPaperProvider>
|
||||
<ObsEditContext.Provider value={{
|
||||
savingPhoto
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ObsEditContext.Provider>
|
||||
</INatPaperProvider>
|
||||
) );
|
||||
|
||||
const renderEvidenceList = evidenceList => renderComponent(
|
||||
<ObsEditProvider>
|
||||
<EvidenceList evidenceList={evidenceList} />
|
||||
</ObsEditProvider>
|
||||
);
|
||||
|
||||
describe( "EvidenceList", ( ) => {
|
||||
it( "should display add evidence button", ( ) => {
|
||||
renderComponent(
|
||||
<EvidenceList
|
||||
evidenceList={observationPhotos}
|
||||
/>
|
||||
);
|
||||
mockObsEditProvider( );
|
||||
renderEvidenceList( observationPhotos );
|
||||
|
||||
expect( screen.getByTestId( "EvidenceList.add" ) ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should display loading wheel if photo is saving", ( ) => {
|
||||
renderComponent(
|
||||
<EvidenceList
|
||||
evidenceList={observationPhotos}
|
||||
savingPhoto
|
||||
/>
|
||||
);
|
||||
mockObsEditProvider( true );
|
||||
renderEvidenceList( observationPhotos );
|
||||
|
||||
expect( screen.getByTestId( "EvidenceList.saving" ) ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should render all observation photos", ( ) => {
|
||||
renderComponent(
|
||||
<EvidenceList
|
||||
evidenceList={observationPhotos}
|
||||
/>
|
||||
);
|
||||
mockObsEditProvider( );
|
||||
renderEvidenceList( observationPhotos );
|
||||
|
||||
expect( screen.getByTestId( `EvidenceList.${observationPhotos[0].photo.url}` ) ).toBeVisible( );
|
||||
expect( screen.getByTestId( `EvidenceList.${observationPhotos[1].photo.url}` ) ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should display an empty list when observation has no observation photos", ( ) => {
|
||||
renderComponent(
|
||||
<EvidenceList
|
||||
evidenceList={[]}
|
||||
/>
|
||||
);
|
||||
mockObsEditProvider( );
|
||||
renderEvidenceList( [] );
|
||||
expect( screen.getByTestId( "EvidenceList.add" ) ).toBeVisible( );
|
||||
expect( screen.queryByTestId( "ObsEdit.photo" ) ).toBeFalsy( );
|
||||
} );
|
||||
|
||||
@@ -1,75 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`IdentificationsCount renders default reliably 1`] = `
|
||||
<View
|
||||
accessibilityLabel="1 identification"
|
||||
accessible={true}
|
||||
style={
|
||||
[
|
||||
[
|
||||
{
|
||||
"flexDirection": "row",
|
||||
},
|
||||
{
|
||||
"alignItems": "center",
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
selectable={false}
|
||||
style={
|
||||
[
|
||||
{
|
||||
"color": "rgba(103, 80, 164, 1)",
|
||||
"fontSize": 14,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
"fontFamily": "INatIcon",
|
||||
"fontStyle": "normal",
|
||||
"fontWeight": "normal",
|
||||
},
|
||||
{},
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
"fontFamily": "Whitney-Light",
|
||||
},
|
||||
[
|
||||
{
|
||||
"fontSize": 14,
|
||||
"lineHeight": 18,
|
||||
},
|
||||
{
|
||||
"fontWeight": "400",
|
||||
},
|
||||
{
|
||||
"color": "#454545",
|
||||
},
|
||||
[
|
||||
{
|
||||
"marginLeft": 6,
|
||||
},
|
||||
],
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
1
|
||||
</Text>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`IdentificationsCount renders filled reliably 1`] = `
|
||||
<View
|
||||
accessibilityLabel="1 identification"
|
||||
accessible={true}
|
||||
@@ -138,6 +69,75 @@ exports[`IdentificationsCount renders filled reliably 1`] = `
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`IdentificationsCount renders filled reliably 1`] = `
|
||||
<View
|
||||
accessibilityLabel="1 identification"
|
||||
accessible={true}
|
||||
style={
|
||||
[
|
||||
[
|
||||
{
|
||||
"flexDirection": "row",
|
||||
},
|
||||
{
|
||||
"alignItems": "center",
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
selectable={false}
|
||||
style={
|
||||
[
|
||||
{
|
||||
"color": "rgba(103, 80, 164, 1)",
|
||||
"fontSize": 14,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
"fontFamily": "INatIcon",
|
||||
"fontStyle": "normal",
|
||||
"fontWeight": "normal",
|
||||
},
|
||||
{},
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
"fontFamily": "Whitney-Light",
|
||||
},
|
||||
[
|
||||
{
|
||||
"fontSize": 14,
|
||||
"lineHeight": 18,
|
||||
},
|
||||
{
|
||||
"fontWeight": "400",
|
||||
},
|
||||
{
|
||||
"color": "#454545",
|
||||
},
|
||||
[
|
||||
{
|
||||
"marginLeft": 6,
|
||||
},
|
||||
],
|
||||
],
|
||||
]
|
||||
}
|
||||
>
|
||||
1
|
||||
</Text>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`IdentificationsCount renders white reliably 1`] = `
|
||||
<View
|
||||
accessibilityLabel="1 identification"
|
||||
@@ -174,7 +174,7 @@ exports[`IdentificationsCount renders white reliably 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
|
||||
@@ -174,7 +174,7 @@ exports[`InlineUser when offline renders reliably 1`] = `
|
||||
}
|
||||
testID="InlineUser.NoInternetPicture"
|
||||
>
|
||||
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
@@ -285,7 +285,7 @@ exports[`InlineUser when user has no icon set renders reliably 1`] = `
|
||||
}
|
||||
testID="InlineUser.FallbackPicture"
|
||||
>
|
||||
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
|
||||
@@ -23,6 +23,12 @@ exports[`ObsGridItem for an observation with a photo should render 1`] = `
|
||||
{
|
||||
"height": 62,
|
||||
},
|
||||
{
|
||||
"borderBottomLeftRadius": 15,
|
||||
"borderBottomRightRadius": 15,
|
||||
"borderTopLeftRadius": 15,
|
||||
"borderTopRightRadius": 15,
|
||||
},
|
||||
{
|
||||
"width": "100%",
|
||||
},
|
||||
@@ -274,6 +280,12 @@ exports[`ObsGridItem for an observation without a photo should render 1`] = `
|
||||
{
|
||||
"height": 62,
|
||||
},
|
||||
{
|
||||
"borderBottomLeftRadius": 15,
|
||||
"borderBottomRightRadius": 15,
|
||||
"borderTopLeftRadius": 15,
|
||||
"borderTopRightRadius": 15,
|
||||
},
|
||||
{
|
||||
"width": "100%",
|
||||
},
|
||||
@@ -310,6 +322,12 @@ exports[`ObsGridItem for an observation without a photo should render 1`] = `
|
||||
{
|
||||
"height": 62,
|
||||
},
|
||||
{
|
||||
"borderBottomLeftRadius": 15,
|
||||
"borderBottomRightRadius": 15,
|
||||
"borderTopLeftRadius": 15,
|
||||
"borderTopRightRadius": 15,
|
||||
},
|
||||
{
|
||||
"width": "100%",
|
||||
},
|
||||
@@ -349,7 +367,7 @@ exports[`ObsGridItem for an observation without a photo should render 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<BVLinearGradient
|
||||
@@ -436,7 +454,7 @@ exports[`ObsGridItem for an observation without a photo should render 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
|
||||
@@ -15,17 +15,15 @@ exports[`TaxonResult should render correctly 1`] = `
|
||||
"justifyContent": "space-between",
|
||||
},
|
||||
{
|
||||
"paddingLeft": 12,
|
||||
"paddingLeft": 16,
|
||||
"paddingRight": 16,
|
||||
},
|
||||
{
|
||||
"paddingBottom": 4,
|
||||
"paddingTop": 4,
|
||||
"paddingBottom": 12,
|
||||
"paddingTop": 12,
|
||||
},
|
||||
{
|
||||
"borderBottomWidth": 0.5,
|
||||
"borderLeftWidth": 0.5,
|
||||
"borderRightWidth": 0.5,
|
||||
"borderTopWidth": 0.5,
|
||||
"borderBottomWidth": 1,
|
||||
},
|
||||
{
|
||||
"borderBottomColor": "#E8E8E8",
|
||||
@@ -110,9 +108,23 @@ exports[`TaxonResult should render correctly 1`] = `
|
||||
{
|
||||
"height": 62,
|
||||
},
|
||||
{
|
||||
"borderBottomLeftRadius": 8,
|
||||
"borderBottomRightRadius": 8,
|
||||
"borderTopLeftRadius": 8,
|
||||
"borderTopRightRadius": 8,
|
||||
},
|
||||
{
|
||||
"width": 62,
|
||||
},
|
||||
[
|
||||
{
|
||||
"borderBottomLeftRadius": 12,
|
||||
"borderBottomRightRadius": 12,
|
||||
"borderTopLeftRadius": 12,
|
||||
"borderTopRightRadius": 12,
|
||||
},
|
||||
],
|
||||
],
|
||||
]
|
||||
}
|
||||
@@ -146,6 +158,12 @@ exports[`TaxonResult should render correctly 1`] = `
|
||||
{
|
||||
"height": 62,
|
||||
},
|
||||
{
|
||||
"borderBottomLeftRadius": 8,
|
||||
"borderBottomRightRadius": 8,
|
||||
"borderTopLeftRadius": 8,
|
||||
"borderTopRightRadius": 8,
|
||||
},
|
||||
{
|
||||
"width": 62,
|
||||
},
|
||||
@@ -191,7 +209,7 @@ exports[`TaxonResult should render correctly 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
@@ -388,7 +406,7 @@ exports[`TaxonResult should render correctly 1`] = `
|
||||
]
|
||||
}
|
||||
>
|
||||
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -438,11 +456,11 @@ exports[`TaxonResult should render correctly 1`] = `
|
||||
[
|
||||
{
|
||||
"marginLeft": 8,
|
||||
"marginRight": 8,
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
testID="undefined.checkmark"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
|
||||
48
tests/unit/components/Suggestions/Attribution.test.js
Normal file
48
tests/unit/components/Suggestions/Attribution.test.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { screen, waitFor } from "@testing-library/react-native";
|
||||
import Attribution from "components/Suggestions/Attribution";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import React from "react";
|
||||
|
||||
import factory, { makeResponse } from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
// Mock api call to observations
|
||||
jest.mock( "inaturalistjs" );
|
||||
|
||||
const mockUsers = [
|
||||
factory( "RemoteObservation", {
|
||||
user: {
|
||||
login: faker.name.fullName( )
|
||||
}
|
||||
} ),
|
||||
factory( "RemoteObservation", {
|
||||
user: {
|
||||
login: faker.name.fullName( )
|
||||
}
|
||||
} )
|
||||
];
|
||||
|
||||
const renderAttribution = ( ) => renderComponent(
|
||||
<Attribution
|
||||
taxonIds={[23456, 35235, 64672]}
|
||||
/>
|
||||
);
|
||||
|
||||
describe( "Attribution", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should show attributions", async ( ) => {
|
||||
expect( inatjs.observations.observers ).not.toHaveBeenCalled( );
|
||||
renderAttribution( );
|
||||
expect( screen.getByTestId( "Attribution.empty" ) ).toBeVisible( );
|
||||
|
||||
await waitFor( ( ) => {
|
||||
inatjs.observations.observers.mockResolvedValue( makeResponse( mockUsers ) );
|
||||
expect( inatjs.observations.observers ).toHaveBeenCalled( );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
110
tests/unit/components/Suggestions/Suggestions.test.js
Normal file
110
tests/unit/components/Suggestions/Suggestions.test.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import {
|
||||
screen, waitFor
|
||||
} from "@testing-library/react-native";
|
||||
import SuggestionsContainer from "components/Suggestions/SuggestionsContainer";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import i18next from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import React from "react";
|
||||
|
||||
import factory, { makeResponse } from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
const mockTaxon = factory( "RemoteTaxon", {
|
||||
name: faker.name.firstName( ),
|
||||
preferred_common_name: faker.name.fullName( ),
|
||||
default_photo: {
|
||||
square_url: faker.image.imageUrl( )
|
||||
}
|
||||
} );
|
||||
|
||||
const mockSuggestionsList = [{
|
||||
combined_score: 90.34,
|
||||
taxon: {
|
||||
...mockTaxon,
|
||||
id: faker.datatype.number( )
|
||||
}
|
||||
}, {
|
||||
combined_score: 30.32,
|
||||
taxon: {
|
||||
...mockTaxon,
|
||||
id: faker.datatype.number( )
|
||||
}
|
||||
}];
|
||||
|
||||
// Mock api call to observations
|
||||
jest.mock( "inaturalistjs" );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
...actualNav,
|
||||
useNavigation: ( ) => ( {
|
||||
addListener: jest.fn( )
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
jest.mock( "sharedHooks/useLocalObservation" );
|
||||
|
||||
const mockUris = [
|
||||
faker.image.imageUrl( ),
|
||||
`${faker.image.imageUrl( )}/400`
|
||||
];
|
||||
|
||||
const mockCreateId = jest.fn( );
|
||||
|
||||
const renderSuggestions = ( loading = false, comment = "" ) => renderComponent(
|
||||
<ObsEditContext.Provider value={{
|
||||
updateObservationKeys: jest.fn( ),
|
||||
photoEvidenceUris: mockUris,
|
||||
createId: mockCreateId,
|
||||
loading,
|
||||
comment,
|
||||
currentObservation: {
|
||||
uuid: faker.datatype.uuid( )
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SuggestionsContainer />
|
||||
</ObsEditContext.Provider>
|
||||
);
|
||||
|
||||
describe( "Suggestions", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
test( "should not have accessibility errors", async ( ) => {
|
||||
renderSuggestions( );
|
||||
|
||||
const suggestions = await screen.findByTestId( "suggestions" );
|
||||
expect( suggestions ).toBeAccessible( );
|
||||
} );
|
||||
|
||||
it( "should show loading wheel while id being created", async ( ) => {
|
||||
renderSuggestions( true );
|
||||
await waitFor( ( ) => {
|
||||
expect( screen.queryByTestId( "Suggestions.ActivityIndicator" ) ).toBeVisible( );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( "shows comment section if observation has comment", ( ) => {
|
||||
renderSuggestions( false, "Comment added to observation" );
|
||||
const commentSection = screen.getByText(
|
||||
i18next.t( "Your-identification-will-be-posted-with-the-following-comment" )
|
||||
);
|
||||
expect( commentSection ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should fetch nearby suggestions for current photo", async ( ) => {
|
||||
renderSuggestions( );
|
||||
await waitFor( ( ) => {
|
||||
inatjs.observations.observers.mockResolvedValue( makeResponse( [] ) );
|
||||
inatjs.computervision.score_image.mockResolvedValue( makeResponse( mockSuggestionsList ) );
|
||||
expect( inatjs.computervision.score_image ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
93
tests/unit/components/Suggestions/SuggestionsList.test.js
Normal file
93
tests/unit/components/Suggestions/SuggestionsList.test.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import SuggestionsList from "components/Suggestions/SuggestionsList";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
import { renderComponent } from "../../../helpers/render";
|
||||
|
||||
const mockTaxon = factory( "RemoteTaxon", {
|
||||
name: faker.name.firstName( ),
|
||||
preferred_common_name: faker.name.fullName( ),
|
||||
default_photo: {
|
||||
square_url: faker.image.imageUrl( )
|
||||
}
|
||||
} );
|
||||
|
||||
const mockSuggestionsList = [{
|
||||
combined_score: 90.34,
|
||||
taxon: {
|
||||
...mockTaxon,
|
||||
id: faker.datatype.number( )
|
||||
}
|
||||
}, {
|
||||
combined_score: 30.32,
|
||||
taxon: {
|
||||
...mockTaxon,
|
||||
id: faker.datatype.number( )
|
||||
}
|
||||
}];
|
||||
|
||||
const mockTaxonSelection = jest.fn( );
|
||||
|
||||
const renderSuggestionsList = ( ) => renderComponent(
|
||||
<SuggestionsList
|
||||
nearbySuggestions={mockSuggestionsList}
|
||||
onTaxonChosen={mockTaxonSelection}
|
||||
/>
|
||||
);
|
||||
|
||||
describe( "SuggestionsList", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
it( "should render a top suggestion", ( ) => {
|
||||
renderSuggestionsList( );
|
||||
const topSuggestion = screen.getByTestId(
|
||||
`SuggestionsList.taxa.${mockSuggestionsList[0].taxon.id}`
|
||||
);
|
||||
expect( topSuggestion ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should render other suggestions", ( ) => {
|
||||
renderSuggestionsList( );
|
||||
const secondSuggestion = screen.getByTestId(
|
||||
`SuggestionsList.taxa.${mockSuggestionsList[1].taxon.id}`
|
||||
);
|
||||
expect( secondSuggestion ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should display empty text if no suggestions are found", ( ) => {
|
||||
renderComponent(
|
||||
<SuggestionsList
|
||||
nearbySuggestions={[]}
|
||||
/>
|
||||
);
|
||||
const emptyText = i18next
|
||||
.t( "iNaturalist-isnt-able-to-provide-a-top-ID-suggestion-for-this-photo" );
|
||||
expect( screen.getByText( emptyText ) ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should display a loading wheel if suggestions are loading", ( ) => {
|
||||
renderComponent(
|
||||
<SuggestionsList
|
||||
nearbySuggestions={[]}
|
||||
loading
|
||||
/>
|
||||
);
|
||||
const loading = screen.getByTestId( "SuggestionsList.loading" );
|
||||
expect( loading ).toBeVisible( );
|
||||
} );
|
||||
|
||||
it( "should create an id when checkmark is pressed", async ( ) => {
|
||||
renderSuggestionsList( );
|
||||
const testID = `SuggestionsList.taxa.${mockSuggestionsList[0].taxon.id}`;
|
||||
const checkmark = screen.getByTestId( `${testID}.checkmark` );
|
||||
expect( checkmark ).toBeVisible( );
|
||||
fireEvent.press( checkmark );
|
||||
expect( mockTaxonSelection ).toHaveBeenCalled( );
|
||||
} );
|
||||
} );
|
||||
@@ -1,9 +1,9 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import { fireEvent, screen } from "@testing-library/react-native";
|
||||
import AddIDContainer from "components/AddID/AddIDContainer";
|
||||
import TaxonSearch from "components/Suggestions/TaxonSearch";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import { t } from "i18next";
|
||||
import i18next from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import INatPaperProvider from "providers/INatPaperProvider";
|
||||
@@ -15,14 +15,6 @@ import { renderComponent } from "../../../helpers/render";
|
||||
jest.mock( "inaturalistjs" );
|
||||
jest.mock( "providers/ObsEditProvider" );
|
||||
|
||||
const mockMutate = jest.fn();
|
||||
jest.mock( "sharedHooks/useAuthenticatedMutation", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
mutate: mockMutate
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock(
|
||||
"components/SharedComponents/ViewWrapper",
|
||||
() => function MockViewWrapper( props ) {
|
||||
@@ -38,23 +30,10 @@ jest.mock(
|
||||
|
||||
const mockTaxon = factory( "RemoteTaxon", {
|
||||
name: faker.name.firstName( ),
|
||||
// rank: "genus",
|
||||
// rank_level: 27,
|
||||
preferred_common_name: faker.name.fullName( ),
|
||||
default_photo: {
|
||||
square_url: faker.image.imageUrl( )
|
||||
}
|
||||
// ancestors: [{
|
||||
// id: faker.datatype.number( ),
|
||||
// preferred_common_name: faker.name.fullName( ),
|
||||
// name: faker.name.fullName( ),
|
||||
// rank: "class"
|
||||
// }],
|
||||
// wikipedia_summary: faker.lorem.paragraph( ),
|
||||
// taxonPhotos: [{
|
||||
// photo: factory( "RemotePhoto" )
|
||||
// }],
|
||||
// wikipedia_url: faker.internet.url( )
|
||||
} );
|
||||
|
||||
const mockTaxaList = [
|
||||
@@ -107,68 +86,60 @@ jest.mock( "react-native-paper", () => {
|
||||
return MockedModule;
|
||||
} );
|
||||
|
||||
const mockRoute = {
|
||||
params: {
|
||||
observationUUID: faker.datatype.uuid( ),
|
||||
createRemoteIdentification: true
|
||||
}
|
||||
};
|
||||
const mockCreateId = jest.fn( );
|
||||
|
||||
const renderAddId = ( ) => renderComponent(
|
||||
const renderTaxonSearch = ( loading = false, comment = "" ) => renderComponent(
|
||||
<ObsEditContext.Provider value={{
|
||||
updateObservationKeys: jest.fn( )
|
||||
updateObservationKeys: jest.fn( ),
|
||||
createId: mockCreateId,
|
||||
loading,
|
||||
comment
|
||||
}}
|
||||
>
|
||||
<AddIDContainer route={mockRoute} />
|
||||
<TaxonSearch />
|
||||
</ObsEditContext.Provider>
|
||||
);
|
||||
|
||||
describe( "AddID", ( ) => {
|
||||
describe( "TaxonSearch", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
test( "should not have accessibility errors", () => {
|
||||
const addID = (
|
||||
const suggestions = (
|
||||
<BottomSheetModalProvider>
|
||||
<INatPaperProvider>
|
||||
<ObsEditContext.Provider value={{
|
||||
updateObservationKeys: jest.fn( )
|
||||
}}
|
||||
>
|
||||
<AddIDContainer route={mockRoute} />
|
||||
<TaxonSearch />
|
||||
</ObsEditContext.Provider>
|
||||
</INatPaperProvider>
|
||||
</BottomSheetModalProvider>
|
||||
);
|
||||
expect( addID ).toBeAccessible( );
|
||||
expect( suggestions ).toBeAccessible( );
|
||||
} );
|
||||
|
||||
it( "should render inside mocked container", ( ) => {
|
||||
renderAddId( );
|
||||
renderTaxonSearch( );
|
||||
expect( screen.getByTestId( "mock-view-no-footer" ) ).toBeTruthy();
|
||||
} );
|
||||
|
||||
it( "show taxon search results", async ( ) => {
|
||||
inatjs.search.mockResolvedValue( makeResponse( mockTaxaList ) );
|
||||
renderAddId( );
|
||||
renderTaxonSearch( );
|
||||
const input = screen.getByTestId( "SearchTaxon" );
|
||||
const taxon = mockTaxaList[0];
|
||||
fireEvent.changeText( input, "Some taxon" );
|
||||
expect( await screen.findByTestId( `Search.taxa.${taxon.id}` ) ).toBeTruthy();
|
||||
} );
|
||||
|
||||
it( "shows loading indicator when taxon is selected", async () => {
|
||||
renderAddId( );
|
||||
const input = screen.getByTestId( "SearchTaxon" );
|
||||
const taxon = mockTaxaList[0];
|
||||
|
||||
fireEvent.changeText( input, "Some taxon" );
|
||||
|
||||
expect( await screen.findByTestId( `Search.taxa.${taxon.id}` ) ).toBeTruthy();
|
||||
const labelText = t( "Checkmark" );
|
||||
const chooseButton = ( await screen.findAllByLabelText( labelText ) )[0];
|
||||
fireEvent.press( chooseButton );
|
||||
await screen.findByTestId( "AddID.ActivityIndicator" );
|
||||
it( "shows comment section if observation has comment", ( ) => {
|
||||
renderTaxonSearch( false, "Comment added to observation in TaxonSearch" );
|
||||
const commentSection = screen.getByText(
|
||||
i18next.t( "Your-identification-will-be-posted-with-the-following-comment" )
|
||||
);
|
||||
expect( commentSection ).toBeVisible( );
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user