mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-04 13:43:34 -04:00
Use authenticated queries for fetch/search requests (#195)
* Use authenticated query for search results * Use search API for fetching places from Settings * Use authenticated query for authorized applications * Use authenticated query to fetch user.me * Move fetch/search api calls into react query format, out of hooks * Update with react query instead of hooks * Fetch list of blocked and muted users with authenticated query * Added Podfile postinstall block to get app running in a Simulator * Use auth query in identify * Upgrade Realm to 11.0.0-rc.0, most recent version that will work with RN 0.68.2 * Upgrade @realm/react library to 0.4.0; fixes initialization error on android due to importing realm * Use authquery for explore provider * ObsDetail wasn't showing the edit button for obs created while signed out * simplified ObsEditHeader so it takes a full observation instead of relying on the ObsEditContext * ObsEdit now accepts an obs UUID as a param and loads that if the context doesn't have a current obs * null checks for API methods, mostly to prevent requests that won't work b/c of missing params Co-authored-by: Ken-ichi Ueda <kenichi.ueda@gmail.com>
This commit is contained in:
committed by
GitHub
parent
c4d9cd4dc6
commit
e81894d406
11
ios/Podfile
11
ios/Podfile
@@ -26,8 +26,13 @@ target 'iNaturalistReactNative' do
|
||||
# you should disable the next line.
|
||||
# use_flipper!()
|
||||
|
||||
# post_install do |installer|
|
||||
# react_native_post_install(installer)
|
||||
# end
|
||||
post_install do |installer|
|
||||
# https://github.com/Agontuk/react-native-geolocation-service/issues/287#issuecomment-980772489
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -10,9 +10,6 @@ PODS:
|
||||
- React-jsi (= 0.68.2)
|
||||
- ReactCommon/turbomodule/core (= 0.68.2)
|
||||
- fmt (6.2.1)
|
||||
- GCDWebServer (3.5.4):
|
||||
- GCDWebServer/Core (= 3.5.4)
|
||||
- GCDWebServer/Core (3.5.4)
|
||||
- glog (0.3.5)
|
||||
- Permission-LocationWhenInUse (3.3.1):
|
||||
- RNPermissions
|
||||
@@ -314,8 +311,7 @@ PODS:
|
||||
- React-jsi (= 0.68.2)
|
||||
- React-logger (= 0.68.2)
|
||||
- React-perflogger (= 0.68.2)
|
||||
- RealmJS (10.21.1):
|
||||
- GCDWebServer
|
||||
- RealmJS (11.0.0-rc.0):
|
||||
- React
|
||||
- RNAudioRecorderPlayer (3.5.1):
|
||||
- React-Core
|
||||
@@ -442,7 +438,6 @@ DEPENDENCIES:
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- fmt
|
||||
- GCDWebServer
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
boost:
|
||||
@@ -568,7 +563,6 @@ SPEC CHECKSUMS:
|
||||
FBLazyVector: a7a655862f6b09625d11c772296b01cd5164b648
|
||||
FBReactNativeSpec: 81ce99032d5b586fddd6a38d450f8595f7e04be4
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
|
||||
glog: 476ee3e89abb49e07f822b48323c51c57124b572
|
||||
Permission-LocationWhenInUse: 006c85c8de0c05b5d8be8e8029e4f6b813270293
|
||||
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
|
||||
@@ -606,7 +600,7 @@ SPEC CHECKSUMS:
|
||||
React-RCTVibration: 79040b92bfa9c3c2d2cb4f57e981164ec7ab9374
|
||||
React-runtimeexecutor: b960b687d2dfef0d3761fbb187e01812ebab8b23
|
||||
ReactCommon: 095366164a276d91ea704ce53cb03825c487a3f2
|
||||
RealmJS: 96af0fcb0bf79530e090067df590650917c27b20
|
||||
RealmJS: 6ee99e016e85a71233f92c64b8255007810b7478
|
||||
RNAudioRecorderPlayer: 308940de4f9d1448a064874fd9d83479ae47c7a7
|
||||
RNCAsyncStorage: d81ee5c3db1060afd49ea7045ad460eff82d2b7d
|
||||
RNCCheckbox: ed1b4ca295475b41e7251ebae046360a703b6eb5
|
||||
@@ -624,6 +618,6 @@ SPEC CHECKSUMS:
|
||||
VisionCamera: c1c171fcdbf18c438987847f785829c5638f3a4c
|
||||
Yoga: 99652481fcd320aefa4a7ef90095b95acd181952
|
||||
|
||||
PODFILE CHECKSUM: 51a6b3d8767f92f6783584e73a5acbcb4074e65d
|
||||
PODFILE CHECKSUM: d1bc9d7f55b5f7449c3ceb6a7622454bc4f8b972
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
97
package-lock.json
generated
97
package-lock.json
generated
@@ -23,7 +23,7 @@
|
||||
"@react-navigation/elements": "^1.3.1",
|
||||
"@react-navigation/native": "^6.0.8",
|
||||
"@react-navigation/native-stack": "^6.5.2",
|
||||
"@realm/react": "^0.2.1",
|
||||
"@realm/react": "^0.4.0",
|
||||
"@tanstack/react-query": "^4.2.1",
|
||||
"apisauce": "^2.1.2",
|
||||
"axios": "^0.25.0",
|
||||
@@ -72,7 +72,7 @@
|
||||
"react-native-vision-camera": "^2.13.0",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-tinder-card": "^1.4.5",
|
||||
"realm": "^10.20.0-beta.3",
|
||||
"realm": "^11.0.0-rc.0",
|
||||
"use-debounce": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3586,21 +3586,26 @@
|
||||
"resolved": "https://registry.npmjs.org/@realm.io/common/-/common-0.1.5.tgz",
|
||||
"integrity": "sha512-Y+UnICLvsPFpe2WOXWIdJUaV3G2qDocN8al/Yz13mYMkjODXHL4VhyfEKR2hvcAubv+7isdegEyYNdo3zQzbFA=="
|
||||
},
|
||||
"node_modules/@realm/common": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@realm/common/-/common-0.1.4.tgz",
|
||||
"integrity": "sha512-bKpIRZIQ4ykribFi0igCwuvf7P4+Ex2XYKqDw1JDe6sCGAaPMwhazooyM6h32fUjtXRTbdAWH2S9JH8Xh/LrqQ=="
|
||||
},
|
||||
"node_modules/@realm/react": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@realm/react/-/react-0.2.1.tgz",
|
||||
"integrity": "sha512-X1UdST8GoTWG6/JVPBSdVgi6UQ2J/9ixXzJ6AFXV6+TomtMtE82/Tb2xI/sU+wTi01y8A96zTnU+IXii9N9Itw==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@realm/react/-/react-0.4.0.tgz",
|
||||
"integrity": "sha512-nWN0izov3rWdWZdJfGPbO2GfsvWn7h4ET4lbcVOjpt3zarldjAvAnIXzUlkZyKZI3anDj0izEWDieILzRY2jiA==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"realm": ">=10"
|
||||
"@realm/common": "^0.1.4",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@babel/runtime": ">=7",
|
||||
"react-native": ">=0.59"
|
||||
"react-native": ">=0.68"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"realm": ">=10"
|
||||
"react": ">=17.0.2",
|
||||
"realm": "^11.0.0-rc || ^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
@@ -5209,6 +5214,26 @@
|
||||
"integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/clang-format": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.8.0.tgz",
|
||||
"integrity": "sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw==",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"glob": "^7.0.0",
|
||||
"resolve": "^1.1.6"
|
||||
},
|
||||
"bin": {
|
||||
"check-clang-format": "bin/check-clang-format.js",
|
||||
"clang-format": "index.js",
|
||||
"git-clang-format": "bin/git-clang-format"
|
||||
}
|
||||
},
|
||||
"node_modules/clang-format/node_modules/async": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
},
|
||||
"node_modules/class-utils": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
|
||||
@@ -14268,20 +14293,21 @@
|
||||
"integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg=="
|
||||
},
|
||||
"node_modules/realm": {
|
||||
"version": "10.21.1",
|
||||
"resolved": "https://registry.npmjs.org/realm/-/realm-10.21.1.tgz",
|
||||
"integrity": "sha512-I+QzOEw478LGPxgSLvF3YlsMCwRNe4d35uKB77tg2rARDYVtq4pW/XVfbvPrJnwPpgJR53stHmkspj0xvJgFUw==",
|
||||
"version": "11.0.0-rc.0",
|
||||
"resolved": "https://registry.npmjs.org/realm/-/realm-11.0.0-rc.0.tgz",
|
||||
"integrity": "sha512-Lnq5oyj0MseFPU0sykz99PPR2STJAlAUzGFjTrHd/rSR3K/MdRN/l7Bw+R1tWLt0Y45cgTdnWUp9u672fJ4LiA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@realm.io/common": "^0.1.4",
|
||||
"bindings": "^1.5.0",
|
||||
"bson": "4.4.1",
|
||||
"clang-format": "^1.6.0",
|
||||
"command-line-args": "^5.1.1",
|
||||
"deepmerge": "2.1.0",
|
||||
"fs-extra": "^4.0.3",
|
||||
"ini": "^1.3.7",
|
||||
"node-addon-api": "4.2.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"node-fetch": "^3.2.6",
|
||||
"node-machine-id": "^1.1.10",
|
||||
"prebuild-install": "^7.0.1",
|
||||
"progress": "^2.0.3",
|
||||
@@ -14298,7 +14324,7 @@
|
||||
"npm": ">=7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": ">=0.64"
|
||||
"react-native": ">=0.66.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native": {
|
||||
@@ -19451,15 +19477,20 @@
|
||||
"resolved": "https://registry.npmjs.org/@realm.io/common/-/common-0.1.5.tgz",
|
||||
"integrity": "sha512-Y+UnICLvsPFpe2WOXWIdJUaV3G2qDocN8al/Yz13mYMkjODXHL4VhyfEKR2hvcAubv+7isdegEyYNdo3zQzbFA=="
|
||||
},
|
||||
"@realm/common": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@realm/common/-/common-0.1.4.tgz",
|
||||
"integrity": "sha512-bKpIRZIQ4ykribFi0igCwuvf7P4+Ex2XYKqDw1JDe6sCGAaPMwhazooyM6h32fUjtXRTbdAWH2S9JH8Xh/LrqQ=="
|
||||
},
|
||||
"@realm/react": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@realm/react/-/react-0.2.1.tgz",
|
||||
"integrity": "sha512-X1UdST8GoTWG6/JVPBSdVgi6UQ2J/9ixXzJ6AFXV6+TomtMtE82/Tb2xI/sU+wTi01y8A96zTnU+IXii9N9Itw==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@realm/react/-/react-0.4.0.tgz",
|
||||
"integrity": "sha512-nWN0izov3rWdWZdJfGPbO2GfsvWn7h4ET4lbcVOjpt3zarldjAvAnIXzUlkZyKZI3anDj0izEWDieILzRY2jiA==",
|
||||
"requires": {
|
||||
"@babel/runtime": ">=7",
|
||||
"@realm/common": "^0.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"react-native": ">=0.59",
|
||||
"realm": ">=10"
|
||||
"react-native": ">=0.68"
|
||||
}
|
||||
},
|
||||
"@sideway/address": {
|
||||
@@ -20685,6 +20716,23 @@
|
||||
"integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==",
|
||||
"dev": true
|
||||
},
|
||||
"clang-format": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.8.0.tgz",
|
||||
"integrity": "sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw==",
|
||||
"requires": {
|
||||
"async": "^3.2.3",
|
||||
"glob": "^7.0.0",
|
||||
"resolve": "^1.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"class-utils": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
|
||||
@@ -27590,19 +27638,20 @@
|
||||
"integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg=="
|
||||
},
|
||||
"realm": {
|
||||
"version": "10.21.1",
|
||||
"resolved": "https://registry.npmjs.org/realm/-/realm-10.21.1.tgz",
|
||||
"integrity": "sha512-I+QzOEw478LGPxgSLvF3YlsMCwRNe4d35uKB77tg2rARDYVtq4pW/XVfbvPrJnwPpgJR53stHmkspj0xvJgFUw==",
|
||||
"version": "11.0.0-rc.0",
|
||||
"resolved": "https://registry.npmjs.org/realm/-/realm-11.0.0-rc.0.tgz",
|
||||
"integrity": "sha512-Lnq5oyj0MseFPU0sykz99PPR2STJAlAUzGFjTrHd/rSR3K/MdRN/l7Bw+R1tWLt0Y45cgTdnWUp9u672fJ4LiA==",
|
||||
"requires": {
|
||||
"@realm.io/common": "^0.1.4",
|
||||
"bindings": "^1.5.0",
|
||||
"bson": "4.4.1",
|
||||
"clang-format": "^1.6.0",
|
||||
"command-line-args": "^5.1.1",
|
||||
"deepmerge": "2.1.0",
|
||||
"fs-extra": "^4.0.3",
|
||||
"ini": "^1.3.7",
|
||||
"node-addon-api": "4.2.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"node-fetch": "^3.2.6",
|
||||
"node-machine-id": "^1.1.10",
|
||||
"prebuild-install": "^7.0.1",
|
||||
"progress": "^2.0.3",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@react-navigation/elements": "^1.3.1",
|
||||
"@react-navigation/native": "^6.0.8",
|
||||
"@react-navigation/native-stack": "^6.5.2",
|
||||
"@realm/react": "^0.2.1",
|
||||
"@realm/react": "^0.4.0",
|
||||
"@tanstack/react-query": "^4.2.1",
|
||||
"apisauce": "^2.1.2",
|
||||
"axios": "^0.25.0",
|
||||
@@ -77,7 +77,7 @@
|
||||
"react-native-vision-camera": "^2.13.0",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-tinder-card": "^1.4.5",
|
||||
"realm": "^10.20.0-beta.3",
|
||||
"realm": "^11.0.0-rc.0",
|
||||
"use-debounce": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -4,11 +4,6 @@ module.exports = {
|
||||
platforms: {
|
||||
android: null // disable Android platform, other platforms will still autolink if provided
|
||||
}
|
||||
},
|
||||
"react-native-vector-icons": {
|
||||
platforms: {
|
||||
ios: null
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
26
src/api/authorizedApplications.js
Normal file
26
src/api/authorizedApplications.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const PARAMS = {
|
||||
fields: "application.official,application.name,created_at"
|
||||
};
|
||||
|
||||
const fetchAuthorizedApplications = async (
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.authorized_applications.search(
|
||||
{ ...PARAMS, ...params },
|
||||
opts
|
||||
);
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchAuthorizedApplications;
|
||||
@@ -1,18 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import MESSAGE_FIELDS from "providers/fields";
|
||||
|
||||
import User from "../models/User";
|
||||
import handleError from "./error";
|
||||
|
||||
const searchMessages = async ( options: Object ): Promise<any> => {
|
||||
const params = {
|
||||
page: 1,
|
||||
fields: MESSAGE_FIELDS
|
||||
};
|
||||
const MESSAGE_FIELDS = {
|
||||
subject: true,
|
||||
body: true,
|
||||
from_user: User.USER_FIELDS,
|
||||
to_user: User.USER_FIELDS
|
||||
};
|
||||
|
||||
const PARAMS = {
|
||||
fields: MESSAGE_FIELDS
|
||||
};
|
||||
|
||||
const searchMessages = async ( params: Object = {}, opts: Object = {} ): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.messages.search( params, options );
|
||||
const { results } = await inatjs.messages.search( { ...PARAMS, ...params }, opts );
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
|
||||
91
src/api/observations.js
Normal file
91
src/api/observations.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import Observation from "../models/Observation";
|
||||
import handleError from "./error";
|
||||
|
||||
const PHOTO_FIELDS = {
|
||||
id: true,
|
||||
attribution: true,
|
||||
license_code: true,
|
||||
url: true
|
||||
};
|
||||
|
||||
const OBSERVATION_PHOTOS_FIELDS = {
|
||||
id: true,
|
||||
photo: PHOTO_FIELDS,
|
||||
position: true,
|
||||
uuid: true
|
||||
};
|
||||
|
||||
const TAXON_FIELDS = {
|
||||
name: true,
|
||||
preferred_common_name: true
|
||||
};
|
||||
|
||||
const FIELDS = {
|
||||
observation_photos: OBSERVATION_PHOTOS_FIELDS,
|
||||
taxon: TAXON_FIELDS
|
||||
};
|
||||
|
||||
const PARAMS = {
|
||||
per_page: 10,
|
||||
fields: FIELDS
|
||||
};
|
||||
|
||||
const REMOTE_OBSERVATION_PARAMS = {
|
||||
fields: "all"
|
||||
};
|
||||
|
||||
const searchObservations = async ( params: Object = {}, opts: Object = {} ): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.observations.search( { ...PARAMS, ...params, ...opts } );
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
const faveObservation = async ( params: Object = {}, opts: Object = {} ): Promise<?number> => {
|
||||
try {
|
||||
return await inatjs.observations.fave( params, opts );
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
const unfaveObservation = async ( params: Object = {}, opts: Object = {} ): Promise<?number> => {
|
||||
try {
|
||||
return await inatjs.observations.unfave( params, opts );
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRemoteObservation = async (
|
||||
uuid: string,
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<?number> => {
|
||||
try {
|
||||
const { results } = await inatjs.observations.fetch(
|
||||
uuid,
|
||||
{ ...REMOTE_OBSERVATION_PARAMS, ...params },
|
||||
opts
|
||||
);
|
||||
if ( results && results.length > 0 ) {
|
||||
return Observation.mimicRealmMappedPropertiesSchema( results[0] );
|
||||
}
|
||||
return null;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
faveObservation,
|
||||
fetchRemoteObservation,
|
||||
searchObservations,
|
||||
unfaveObservation
|
||||
};
|
||||
20
src/api/places.js
Normal file
20
src/api/places.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const PARAMS = {
|
||||
fields: "display_name"
|
||||
};
|
||||
|
||||
const fetchPlace = async ( id: number, params: Object = {}, opts: Object = {} ): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.places.fetch( id, { ...PARAMS, ...params, ...opts } );
|
||||
return results[0];
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchPlace;
|
||||
45
src/api/projects.js
Normal file
45
src/api/projects.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const FIELDS = {
|
||||
title: true,
|
||||
icon: true,
|
||||
header_image_url: true,
|
||||
description: true
|
||||
};
|
||||
|
||||
const PARAMS = {
|
||||
fields: FIELDS
|
||||
};
|
||||
|
||||
const fetchProjects = async (
|
||||
id: number,
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.projects.fetch( id, { ...PARAMS, ...params }, opts );
|
||||
return results[0];
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
const searchProjects = async ( params: Object = {}, opts: Object = {} ): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.projects.search( { ...PARAMS, ...params }, opts );
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export default searchProjects;
|
||||
|
||||
export {
|
||||
fetchProjects,
|
||||
searchProjects
|
||||
};
|
||||
26
src/api/providerAuthorizations.js
Normal file
26
src/api/providerAuthorizations.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const PARAMS = {
|
||||
fields: "provider_name,created_at"
|
||||
};
|
||||
|
||||
const fetchProviderAuthorizations = async (
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.provider_authorizations.search(
|
||||
{ ...PARAMS, ...params },
|
||||
opts
|
||||
);
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchProviderAuthorizations;
|
||||
21
src/api/relationships.js
Normal file
21
src/api/relationships.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const PARAMS = {
|
||||
fields: "all",
|
||||
per_page: 10
|
||||
};
|
||||
|
||||
const fetchRelationships = async ( params: Object = {}, opts: Object = {} ): Promise<any> => {
|
||||
try {
|
||||
const response = await inatjs.relationships.search( { ...PARAMS, ...params }, opts );
|
||||
return response;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchRelationships;
|
||||
32
src/api/search.js
Normal file
32
src/api/search.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const mappedRecords = {
|
||||
taxa: "taxon",
|
||||
places: "place",
|
||||
users: "user",
|
||||
projects: "project"
|
||||
};
|
||||
|
||||
const PARAMS = {
|
||||
per_page: 10,
|
||||
fields: "all"
|
||||
};
|
||||
|
||||
const fetchSearchResults = async ( params: Object = {}, opts: Object = {} ): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.search( { ...PARAMS, ...params }, opts );
|
||||
const records = results.map( result => {
|
||||
const recordType = mappedRecords[params.sources];
|
||||
return result[recordType];
|
||||
} );
|
||||
return records;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchSearchResults;
|
||||
105
src/api/users.js
Normal file
105
src/api/users.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import handleError from "./error";
|
||||
|
||||
const PARAMS = {
|
||||
fields: "all"
|
||||
};
|
||||
|
||||
const MEMBER_PROJECT_FIELDS = {
|
||||
title: true,
|
||||
icon: true
|
||||
};
|
||||
|
||||
const MEMBER_PROJECT_PARAMS = {
|
||||
per_page: 10,
|
||||
fields: MEMBER_PROJECT_FIELDS
|
||||
};
|
||||
|
||||
const REMOTE_USER_FIELDS = {
|
||||
name: true,
|
||||
login: true,
|
||||
icon_url: true,
|
||||
created_at: true,
|
||||
roles: true,
|
||||
site_id: true,
|
||||
description: true,
|
||||
updated_at: true,
|
||||
species_count: true,
|
||||
observations_count: true,
|
||||
identifications_count: true,
|
||||
journal_posts_count: true,
|
||||
site: true
|
||||
};
|
||||
|
||||
const REMOTE_USER_PARAMS = {
|
||||
fields: REMOTE_USER_FIELDS
|
||||
};
|
||||
|
||||
const fetchUserMe = async ( params: Object = {}, opts: Object = {} ): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.users.me( { ...PARAMS, ...params, ...opts } );
|
||||
return results[0];
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMemberProjects = async ( params: Object = {}, opts: Object = {} ): Promise<any> => {
|
||||
try {
|
||||
const { results } = await inatjs.users.projects( {
|
||||
...MEMBER_PROJECT_PARAMS,
|
||||
...params,
|
||||
...opts
|
||||
} );
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRemoteUser = async (
|
||||
id: number,
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<any> => {
|
||||
if ( !id ) return null;
|
||||
try {
|
||||
const { results } = await inatjs.users.fetch( id, {
|
||||
...REMOTE_USER_PARAMS,
|
||||
...params,
|
||||
...opts
|
||||
} );
|
||||
return results;
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRemoteUsers = async (
|
||||
ids: Array<number>,
|
||||
params: Object = {},
|
||||
opts: Object = {}
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const responses = await Promise.all( ids.map(
|
||||
userId => inatjs.users.fetch( userId, {
|
||||
...REMOTE_USER_PARAMS,
|
||||
...params,
|
||||
...opts
|
||||
} )
|
||||
) );
|
||||
return responses.map( r => r.results[0] );
|
||||
} catch ( e ) {
|
||||
return handleError( e );
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
fetchMemberProjects,
|
||||
fetchRemoteUser,
|
||||
fetchRemoteUsers,
|
||||
fetchUserMe
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
// flow
|
||||
|
||||
import fetchSearchResults from "api/search";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
@@ -8,8 +9,9 @@ import { Image } from "react-native";
|
||||
// and allows users to input immediately instead of first tapping the dropdown
|
||||
// this is a placeholder to get functionality working
|
||||
import DropDownPicker from "react-native-dropdown-picker";
|
||||
import useRemoteSearchResults from "sharedHooks/useRemoteSearchResults";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { imageStyles, viewStyles } from "styles/explore/explore";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
type Props = {
|
||||
searchQuery: string,
|
||||
@@ -38,7 +40,18 @@ const DropdownPicker = ( {
|
||||
zIndex,
|
||||
zIndexInverse
|
||||
}: Props ): Node => {
|
||||
const searchResults = useRemoteSearchResults( searchQuery, sources );
|
||||
// So we'll start searching only once the user finished typing
|
||||
const [finalSearch] = useDebounce( searchQuery, 500 );
|
||||
|
||||
const {
|
||||
data: searchResults
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchSearchResults", finalSearch],
|
||||
optsWithAuth => fetchSearchResults( {
|
||||
q: finalSearch,
|
||||
sources
|
||||
}, optsWithAuth )
|
||||
);
|
||||
|
||||
const placesItem = place => ( {
|
||||
label: place.name,
|
||||
@@ -75,6 +88,10 @@ const DropdownPicker = ( {
|
||||
} );
|
||||
|
||||
const displayItems = ( ) => {
|
||||
if ( finalSearch === "" ) {
|
||||
return [];
|
||||
}
|
||||
if ( !searchResults ) { return []; }
|
||||
if ( sources === "places" ) {
|
||||
return searchResults.map( item => placesItem( item ) );
|
||||
} if ( sources === "taxa" ) {
|
||||
@@ -87,6 +104,9 @@ const DropdownPicker = ( {
|
||||
return [];
|
||||
};
|
||||
|
||||
// TODO: change to the same style of dropdown as in SettingsRelationships?
|
||||
// this should be standardized throughout the app
|
||||
|
||||
return (
|
||||
<DropDownPicker
|
||||
onClose={onClose}
|
||||
|
||||
@@ -68,7 +68,7 @@ const GridItem = ( {
|
||||
testID="ObsList.photo"
|
||||
/>
|
||||
<Image
|
||||
source={{ uri: item.user.icon_url }}
|
||||
source={{ uri: item?.user?.icon_url }}
|
||||
style={imageStyles.userImage}
|
||||
testID="ObsList.identifierPhoto"
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import { searchObservations } from "api/observations";
|
||||
import DropdownPicker from "components/Explore/DropdownPicker";
|
||||
import ViewWithFooter from "components/SharedComponents/ViewWithFooter";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { viewStyles } from "styles/identify/identify";
|
||||
|
||||
import Observation from "../../models/Observation";
|
||||
import CardSwipeView from "./CardSwipeView";
|
||||
import GridView from "./GridView";
|
||||
import useObservations from "./hooks/useObservations";
|
||||
|
||||
const Identify = ( ): Node => {
|
||||
const [view, setView] = React.useState( "grid" );
|
||||
@@ -18,7 +20,28 @@ const Identify = ( ): Node => {
|
||||
const [placeId, setPlaceId] = useState( null );
|
||||
const [taxon, setTaxon] = useState( "" );
|
||||
const [taxonId, setTaxonId] = useState( null );
|
||||
const { observations, loading } = useObservations( placeId, taxonId );
|
||||
|
||||
const searchParams = {
|
||||
reviewed: false,
|
||||
fields: Observation.FIELDS
|
||||
};
|
||||
|
||||
if ( placeId ) {
|
||||
// $FlowIgnore
|
||||
searchParams.place_id = placeId;
|
||||
}
|
||||
if ( taxonId ) {
|
||||
// $FlowIgnore
|
||||
searchParams.taxon_id = taxonId;
|
||||
}
|
||||
|
||||
const {
|
||||
data: observations,
|
||||
isLoading
|
||||
} = useAuthenticatedQuery(
|
||||
["searchObservations"],
|
||||
optsWithAuth => searchObservations( searchParams, optsWithAuth )
|
||||
);
|
||||
|
||||
const updatePlaceId = getValue => setPlaceId( getValue( ) );
|
||||
const updateTaxonId = getValue => setTaxonId( getValue( ) );
|
||||
@@ -34,7 +57,7 @@ const Identify = ( ): Node => {
|
||||
}
|
||||
return (
|
||||
<GridView
|
||||
loading={loading}
|
||||
loading={isLoading}
|
||||
observationList={observations}
|
||||
testID="Identify.observationGrid"
|
||||
/>
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import Observation from "../../../models/Observation";
|
||||
|
||||
const useObservations = ( placeId: ?string, taxonId: ?number ): {
|
||||
observations: Array<Object>,
|
||||
loading: boolean
|
||||
} => {
|
||||
const [loading, setLoading] = useState( false );
|
||||
const [observations, setObservations] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchObservations = async ( ) => {
|
||||
setLoading( true );
|
||||
try {
|
||||
const params = {
|
||||
reviewed: false,
|
||||
// viewer_id: 1132118,
|
||||
// locale: null,
|
||||
// ttl: -1,
|
||||
// order_by: "random",
|
||||
// quality_grade: "any",
|
||||
fields: Observation.FIELDS
|
||||
};
|
||||
if ( placeId ) {
|
||||
// $FlowFixMe
|
||||
params.place_id = placeId;
|
||||
}
|
||||
if ( taxonId ) {
|
||||
// $FlowFixMe
|
||||
params.taxon_id = taxonId;
|
||||
}
|
||||
// $FlowFixMe
|
||||
params.fields.observation_photos.photo.medium_url = true;
|
||||
|
||||
const response = await inatjs.observations.search( params );
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setLoading( false );
|
||||
setObservations( results );
|
||||
} catch ( e ) {
|
||||
setLoading( false );
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch observations for identify:", JSON.stringify( e.response ) );
|
||||
}
|
||||
};
|
||||
|
||||
// this is for performance, so we're not searching the entire globe and all organisms
|
||||
if ( taxonId || placeId ) {
|
||||
fetchObservations( );
|
||||
}
|
||||
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [placeId, taxonId] );
|
||||
|
||||
return {
|
||||
observations,
|
||||
loading
|
||||
};
|
||||
};
|
||||
|
||||
export default useObservations;
|
||||
@@ -4,22 +4,25 @@ import searchMessages from "api/messages";
|
||||
import ViewWithFooter from "components/SharedComponents/ViewWithFooter";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import useQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
|
||||
import MessageList from "./MessageList";
|
||||
|
||||
const Messages = ( ): Node => {
|
||||
const {
|
||||
data: messages,
|
||||
data,
|
||||
isLoading
|
||||
} = useQuery( ["searchMessages"], searchMessages );
|
||||
} = useAuthenticatedQuery(
|
||||
["searchMessages"],
|
||||
optsWithAuth => searchMessages( { page: 1 }, optsWithAuth )
|
||||
);
|
||||
// TODO: Reload when accessing again
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<MessageList
|
||||
loading={isLoading}
|
||||
messageList={messages}
|
||||
messageList={data}
|
||||
testID="Messages.messages"
|
||||
/>
|
||||
</ViewWithFooter>
|
||||
|
||||
@@ -5,8 +5,9 @@ import {
|
||||
BottomSheetTextInput
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { faveObservation, fetchRemoteObservation, unfaveObservation } from "api/observations";
|
||||
import createIdentification from "components/Identify/helpers/createIdentification";
|
||||
import { getUser } from "components/LoginSignUp/AuthenticationService";
|
||||
import Button from "components/SharedComponents/Buttons/Button";
|
||||
import PhotoScroll from "components/SharedComponents/PhotoScroll";
|
||||
import QualityBadge from "components/SharedComponents/QualityBadge";
|
||||
@@ -30,6 +31,8 @@ import { ActivityIndicator, Button as IconButton } from "react-native-paper";
|
||||
import IconMaterial from "react-native-vector-icons/MaterialIcons";
|
||||
import { formatObsListTime } from "sharedHelpers/dateAndTime";
|
||||
import useApiToken from "sharedHooks/useApiToken";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import useCurrentUser from "sharedHooks/useCurrentUser";
|
||||
import colors from "styles/colors";
|
||||
import { imageStyles, textStyles, viewStyles } from "styles/obsDetails/obsDetails";
|
||||
|
||||
@@ -40,8 +43,6 @@ import ActivityTab from "./ActivityTab";
|
||||
import DataTab from "./DataTab";
|
||||
import checkCamelAndSnakeCase from "./helpers/checkCamelAndSnakeCase";
|
||||
import createComment from "./helpers/createComment";
|
||||
import faveObservation from "./helpers/faveObservation";
|
||||
import useRemoteObservation from "./hooks/useRemoteObservation";
|
||||
import ObsDetailsHeader from "./ObsDetailsHeader";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
@@ -54,13 +55,15 @@ LogBox.ignoreLogs( [
|
||||
] );
|
||||
|
||||
const ObsDetails = ( ): Node => {
|
||||
const currentUser = useCurrentUser( );
|
||||
const userId = currentUser?.id;
|
||||
const { t } = useTranslation( );
|
||||
const [refetch, setRefetch] = useState( false );
|
||||
const [showCommentBox, setShowCommentBox] = useState( false );
|
||||
const [comment, setComment] = useState( "" );
|
||||
const { addObservations } = useContext( ObsEditContext );
|
||||
const { params } = useRoute( );
|
||||
let { observation } = params;
|
||||
const { uuid } = params;
|
||||
const [tab, setTab] = useState( 0 );
|
||||
const navigation = useNavigation( );
|
||||
const [ids, setIds] = useState( [] );
|
||||
@@ -68,9 +71,30 @@ const ObsDetails = ( ): Node => {
|
||||
const [addingComment, setAddingComment] = useState( false );
|
||||
const [snapPoint, setSnapPoint] = useState( 100 );
|
||||
const apiToken = useApiToken( );
|
||||
const [localObservation, setLocalObservation] = useState( null );
|
||||
|
||||
const queryClient = useQueryClient( );
|
||||
|
||||
const {
|
||||
data: remoteObservation
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchRemoteObservation", uuid],
|
||||
optsWithAuth => fetchRemoteObservation( uuid, { }, optsWithAuth )
|
||||
);
|
||||
|
||||
const realm = useRealm( );
|
||||
|
||||
useEffect( ( ) => {
|
||||
setLocalObservation( realm?.objectForPrimaryKey( "Observation", uuid ) );
|
||||
}, [realm, uuid] );
|
||||
|
||||
const observation = localObservation || remoteObservation;
|
||||
|
||||
const taxon = observation?.taxon;
|
||||
const user = observation?.user;
|
||||
const faves = observation?.faves;
|
||||
const currentUserFaved = faves?.length > 0 ? faves.find( fave => fave.user.id === userId ) : null;
|
||||
|
||||
// Clear the comment in a timeout so it doesn't trigger a re-render of the
|
||||
// text input *after* the bottom sheet modal gets dismissed, b/c that seems
|
||||
// to re-render the bottom sheet in a presented state, making it hard to
|
||||
@@ -115,15 +139,6 @@ const ObsDetails = ( ): Node => {
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
// TODO: we'll probably need to redo this logic a bit now that we're
|
||||
// passing an observation via navigation instead of reopening realm
|
||||
const { remoteObservation, currentUserFaved } = useRemoteObservation( observation, refetch );
|
||||
|
||||
/* TODO - removed this since otherwise refreshing new comments will not work (will
|
||||
always use old local copy that doesn't include new comments) */
|
||||
if ( remoteObservation ) {
|
||||
observation = remoteObservation;
|
||||
}
|
||||
const showActivityTab = ( ) => setTab( 0 );
|
||||
const showDataTab = ( ) => setTab( 1 );
|
||||
|
||||
@@ -132,33 +147,31 @@ const ObsDetails = ( ): Node => {
|
||||
useEffect( () => {
|
||||
const markViewedLocally = async ( ) => {
|
||||
if ( !apiToken ) return;
|
||||
const existingObs = realm?.objectForPrimaryKey( "Observation", observation.uuid );
|
||||
const existingObs = realm?.objectForPrimaryKey( "Observation", uuid );
|
||||
if ( !existingObs ) { return; }
|
||||
realm?.write( ( ) => {
|
||||
existingObs.viewed = true;
|
||||
} );
|
||||
};
|
||||
if ( observation ) { setIds( Array.from( observation.identifications ) ); }
|
||||
if ( observation.viewed === false ) {
|
||||
Observation.markObservationUpdatesViewed( observation.uuid, apiToken );
|
||||
if ( observation?.viewed === false ) {
|
||||
Observation.markObservationUpdatesViewed( uuid, apiToken );
|
||||
markViewedLocally( );
|
||||
}
|
||||
}, [apiToken, observation, realm] );
|
||||
}, [apiToken, observation, uuid, realm] );
|
||||
|
||||
if ( !observation ) { return null; }
|
||||
|
||||
const comments = Array.from( observation.comments );
|
||||
const photos = _.compact( Array.from( observation.observationPhotos ).map( op => op.photo ) );
|
||||
const { taxon, uuid, user } = observation;
|
||||
|
||||
const onIDAdded = async identification => {
|
||||
// Add temporary ID to observation.identifications ("ghosted" ID, while we're trying to add it)
|
||||
const currentUser = await getUser();
|
||||
const newId = {
|
||||
body: identification.body,
|
||||
taxon: identification.taxon,
|
||||
user: {
|
||||
id: currentUser?.id,
|
||||
id: userId,
|
||||
login: currentUser?.login,
|
||||
signedIn: true
|
||||
},
|
||||
@@ -175,7 +188,7 @@ const ObsDetails = ( ): Node => {
|
||||
|
||||
try {
|
||||
const results = await createIdentification( {
|
||||
observation_id: observation.uuid,
|
||||
observation_id: uuid,
|
||||
taxon_id: newId.taxon.id,
|
||||
body: newId.body
|
||||
} );
|
||||
@@ -207,7 +220,7 @@ const ObsDetails = ( ): Node => {
|
||||
}
|
||||
};
|
||||
|
||||
const navToUserProfile = userId => navigation.navigate( "UserProfile", { userId } );
|
||||
const navToUserProfile = id => navigation.navigate( "UserProfile", { userId: id } );
|
||||
const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", { id: taxon.id } );
|
||||
const navToAddID = ( ) => {
|
||||
addObservations( [observation] );
|
||||
@@ -274,12 +287,15 @@ const ObsDetails = ( ): Node => {
|
||||
);
|
||||
|
||||
const faveOrUnfave = async ( ) => {
|
||||
// TODO: fix fave/unfave functionality with useMutation
|
||||
if ( currentUserFaved ) {
|
||||
await faveObservation( uuid, "unfave" );
|
||||
setRefetch( !refetch );
|
||||
await unfaveObservation( { uuid } );
|
||||
setRefetch( true );
|
||||
queryClient.invalidateQueries( ["fetchRemoteObservation"] );
|
||||
} else {
|
||||
await faveObservation( uuid, "fave" );
|
||||
setRefetch( !refetch );
|
||||
await faveObservation( { uuid } );
|
||||
setRefetch( true );
|
||||
queryClient.invalidateQueries( ["fetchRemoteObservation"] );
|
||||
}
|
||||
};
|
||||
|
||||
@@ -290,7 +306,7 @@ const ObsDetails = ( ): Node => {
|
||||
return (
|
||||
<BottomSheetModalProvider>
|
||||
<ViewWithFooter>
|
||||
<ObsDetailsHeader observationUUID={uuid} />
|
||||
<ObsDetailsHeader observation={observation} />
|
||||
<ScrollView
|
||||
testID={`ObsDetails.${uuid}`}
|
||||
contentContainerStyle={viewStyles.scrollView}
|
||||
|
||||
@@ -2,44 +2,37 @@
|
||||
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Button, Headline } from "react-native-paper";
|
||||
import useCurrentUser from "sharedHooks/useCurrentUser";
|
||||
import colors from "styles/colors";
|
||||
import { viewStyles } from "styles/obsDetails/obsDetailsHeader";
|
||||
|
||||
type Props = {
|
||||
observationUUID: string
|
||||
observation: ?Object
|
||||
}
|
||||
|
||||
const ObsDetailsHeader = ( { observationUUID }: Props ): Node => {
|
||||
const [isLocal, setIsLocal] = useState( null );
|
||||
const { openSavedObservation } = React.useContext( ObsEditContext );
|
||||
|
||||
const ObsDetailsHeader = ( { observation }: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const navigation = useNavigation( );
|
||||
const currentUser = useCurrentUser( );
|
||||
const obsCreatedLocally = observation?.id === null;
|
||||
const obsOwnedByCurrentUser = observation?.user?.id === currentUser?.id;
|
||||
|
||||
const navToObsEdit = ( ) => navigation.navigate( "ObsEdit" );
|
||||
|
||||
useEffect( ( ) => {
|
||||
const checkForLocalObservation = async ( ) => {
|
||||
const isLocalObservation = await openSavedObservation( observationUUID );
|
||||
setIsLocal( isLocalObservation );
|
||||
};
|
||||
|
||||
navigation.addListener( "focus", ( ) => {
|
||||
checkForLocalObservation( );
|
||||
} );
|
||||
}, [observationUUID, openSavedObservation, navigation] );
|
||||
const navToObsEdit = ( ) => navigation.navigate( "ObsEdit", { uuid: observation?.uuid } );
|
||||
|
||||
return (
|
||||
<View style={viewStyles.headerRow}>
|
||||
<HeaderBackButton onPress={( ) => navigation.goBack( )} />
|
||||
<Headline>{t( "Observation" )}</Headline>
|
||||
{isLocal ? <Button icon="pencil" onPress={navToObsEdit} textColor={colors.gray} /> : <View />}
|
||||
{
|
||||
( obsCreatedLocally || obsOwnedByCurrentUser )
|
||||
? <Button icon="pencil" onPress={navToObsEdit} textColor={colors.gray} />
|
||||
: <View />
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { getJWTToken } from "components/LoginSignUp/AuthenticationService";
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
const faveObservation = async ( uuid: string, endpoint: string ): Promise<?number> => {
|
||||
const apiToken = await getJWTToken( false );
|
||||
|
||||
try {
|
||||
const params = {
|
||||
uuid
|
||||
};
|
||||
const options = {
|
||||
api_token: apiToken
|
||||
};
|
||||
|
||||
const response = await inatjs.observations[endpoint]( params, options );
|
||||
return response.total_results;
|
||||
} catch ( e ) {
|
||||
console.log( "Couldn't fave observation:", JSON.stringify( e.response ) );
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default faveObservation;
|
||||
@@ -1,84 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { getUsername } from "components/LoginSignUp/AuthenticationService";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import Observation from "../../../models/Observation";
|
||||
import User from "../../../models/User";
|
||||
|
||||
const useRemoteObservation = ( observation: Object, refetch: boolean ): Object => {
|
||||
const [remoteObservation, setRemoteObservation] = useState( null );
|
||||
const [isConnected, setIsConnected] = useState( null );
|
||||
const [currentUserFaved, setCurrentUserFaved] = useState( null );
|
||||
const prevRefetch = useRef( refetch ).current;
|
||||
|
||||
useEffect( ( ) => {
|
||||
const unsubscribe = NetInfo.addEventListener( state => {
|
||||
setIsConnected( state.isConnected );
|
||||
} );
|
||||
|
||||
// Unsubscribe
|
||||
unsubscribe( );
|
||||
}, [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
|
||||
const fetchObservation = async ( ) => {
|
||||
try {
|
||||
const currentUserLogin = await getUsername( );
|
||||
|
||||
const params = {
|
||||
fields: Observation.FIELDS
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
params.fields.application = {
|
||||
icon: true,
|
||||
name: true,
|
||||
url: true
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
params.fields.faves = {
|
||||
user: User.USER_FIELDS
|
||||
};
|
||||
|
||||
const response = await inatjs.observations.fetch( observation.uuid, params );
|
||||
const { results } = response;
|
||||
const obs = Observation.mimicRealmMappedPropertiesSchema( results[0] );
|
||||
if ( !isCurrent ) { return; }
|
||||
if ( obs.faves ) {
|
||||
const userFavedObs = obs.faves.find( fave => fave.user.login === currentUserLogin );
|
||||
if ( userFavedObs ) {
|
||||
setCurrentUserFaved( true );
|
||||
} else {
|
||||
setCurrentUserFaved( false );
|
||||
}
|
||||
}
|
||||
setRemoteObservation( obs );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( `Couldn't fetch observation with uuid ${observation.uuid}: `, e.message );
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: probably need an error message for no connectivity
|
||||
if ( isConnected && ( !observation || refetch !== prevRefetch ) ) {
|
||||
fetchObservation( );
|
||||
}
|
||||
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [observation, isConnected, refetch, prevRefetch] );
|
||||
|
||||
return {
|
||||
remoteObservation,
|
||||
currentUserFaved
|
||||
};
|
||||
};
|
||||
|
||||
export default useRemoteObservation;
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BottomSheetModalProvider
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import fetchSearchResults from "api/search";
|
||||
import ViewNoFooter from "components/SharedComponents/ViewNoFooter";
|
||||
import * as React from "react";
|
||||
import { useRef, useState } from "react";
|
||||
@@ -13,7 +14,9 @@ import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable, TextInput as NativeTextInput, TouchableOpacity,
|
||||
Pressable,
|
||||
TextInput as NativeTextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import {
|
||||
@@ -21,7 +24,7 @@ import {
|
||||
} from "react-native-paper";
|
||||
import uuid from "react-native-uuid";
|
||||
import IconMaterial from "react-native-vector-icons/MaterialIcons";
|
||||
import useRemoteSearchResults from "sharedHooks/useRemoteSearchResults";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import colors from "styles/colors";
|
||||
import { textStyles, viewStyles } from "styles/obsDetails/addID";
|
||||
|
||||
@@ -50,11 +53,17 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
const { onIDAdded, goBackOnSave, hideComment } = route.params;
|
||||
const bottomSheetModalRef = useRef( null );
|
||||
const [taxonSearch, setTaxonSearch] = useState( "" );
|
||||
const taxonList = useRemoteSearchResults(
|
||||
taxonSearch,
|
||||
"taxa",
|
||||
"taxon.name,taxon.preferred_common_name,taxon.default_photo.square_url,taxon.rank"
|
||||
const {
|
||||
data: taxonList
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchSearchResults", taxonSearch],
|
||||
optsWithAuth => fetchSearchResults( {
|
||||
q: taxonSearch,
|
||||
sources: "taxa",
|
||||
fields: "taxon.name,taxon.preferred_common_name,taxon.default_photo.square_url,taxon.rank"
|
||||
}, optsWithAuth )
|
||||
);
|
||||
|
||||
const navigation = useNavigation( );
|
||||
|
||||
const renderBackdrop = props => (
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import PlaceholderText from "components/PlaceholderText";
|
||||
import Button from "components/SharedComponents/Buttons/Button";
|
||||
import PhotoCarousel from "components/SharedComponents/PhotoCarousel";
|
||||
import ViewNoFooter from "components/SharedComponents/ViewNoFooter";
|
||||
import { t } from "i18next";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator, FlatList, Image, Pressable, Text, View
|
||||
} from "react-native";
|
||||
import { Searchbar } from "react-native-paper";
|
||||
import useLoggedIn from "sharedHooks/useLoggedIn";
|
||||
import useRemoteObsEditSearchResults from "sharedHooks/useRemoteSearchResults";
|
||||
import { textStyles, viewStyles } from "styles/obsEdit/cvSuggestions";
|
||||
|
||||
import useCVSuggestions from "./hooks/useCVSuggestions";
|
||||
|
||||
const CVSuggestions = ( ): Node => {
|
||||
const {
|
||||
observations,
|
||||
currentObsIndex,
|
||||
updateTaxon
|
||||
} = useContext( ObsEditContext );
|
||||
const navigation = useNavigation( );
|
||||
const [showSeenNearby, setShowSeenNearby] = useState( true );
|
||||
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState( 0 );
|
||||
const [q, setQ] = React.useState( "" );
|
||||
const list = useRemoteObsEditSearchResults( q, "taxa", "all" );
|
||||
const isLoggedIn = useLoggedIn( );
|
||||
|
||||
const currentObs = observations[currentObsIndex];
|
||||
const hasPhotos = currentObs.observationPhotos;
|
||||
const { suggestions, status } = useCVSuggestions(
|
||||
currentObs,
|
||||
showSeenNearby,
|
||||
selectedPhotoIndex
|
||||
);
|
||||
|
||||
const renderNavButtons = ( updateIdentification, id ) => {
|
||||
const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", { id } );
|
||||
return (
|
||||
<View>
|
||||
<Pressable onPress={navToTaxonDetails}>
|
||||
<PlaceholderText text="info" />
|
||||
</Pressable>
|
||||
<PlaceholderText text="compare tool" />
|
||||
<Pressable onPress={updateIdentification}>
|
||||
<PlaceholderText text="confirm id" />
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSuggestions = ( { item } ) => {
|
||||
const taxon = item && item.taxon;
|
||||
// destructuring so this doesn't cause a crash
|
||||
const mediumUrl = ( taxon && taxon.taxon_photos && taxon.taxon_photos[0].photo )
|
||||
? taxon.taxon_photos[0].photo.medium_url
|
||||
: null;
|
||||
const uri = { uri: mediumUrl };
|
||||
|
||||
const updateIdentification = ( ) => updateTaxon( taxon );
|
||||
|
||||
return (
|
||||
<View style={viewStyles.row}>
|
||||
<Image
|
||||
source={uri}
|
||||
style={viewStyles.imageBackground}
|
||||
/>
|
||||
<View style={viewStyles.obsDetailsColumn}>
|
||||
<Text style={textStyles.text}>{taxon.preferred_common_name}</Text>
|
||||
<Text style={textStyles.text}>{taxon.name}</Text>
|
||||
{showSeenNearby
|
||||
&& <PlaceholderText style={[textStyles.greenText]} text="seen nearby" />}
|
||||
</View>
|
||||
{renderNavButtons( updateIdentification, taxon.id )}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSearchResults = ( { item } ) => {
|
||||
const uri = { uri: item.default_photo.square_url };
|
||||
|
||||
const newTaxon = {
|
||||
name: item.name,
|
||||
preferred_common_name: item.preferred_common_name,
|
||||
id: item.id
|
||||
};
|
||||
|
||||
const updateIdentification = ( ) => updateTaxon( newTaxon );
|
||||
|
||||
return (
|
||||
<View style={viewStyles.row}>
|
||||
<Image
|
||||
source={uri}
|
||||
style={viewStyles.imageBackground}
|
||||
/>
|
||||
<View style={viewStyles.obsDetailsColumn}>
|
||||
<Text style={textStyles.text}>{item.preferred_common_name}</Text>
|
||||
<Text style={textStyles.text}>{item.name}</Text>
|
||||
</View>
|
||||
{renderNavButtons( updateIdentification, item.id )}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleSeenNearby = ( ) => setShowSeenNearby( !showSeenNearby );
|
||||
|
||||
const emptySuggestionsList = ( ) => {
|
||||
if ( !isLoggedIn ) {
|
||||
return (
|
||||
<PlaceholderText
|
||||
style={[textStyles.explainerText]}
|
||||
text="you must be logged in to see computer vision suggestions"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if ( status === "no_results" ) {
|
||||
return (
|
||||
<PlaceholderText
|
||||
style={[textStyles.explainerText]}
|
||||
text="no computervision suggestions found"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ActivityIndicator />;
|
||||
};
|
||||
|
||||
const displaySuggestions = ( ) => (
|
||||
<FlatList
|
||||
data={suggestions}
|
||||
renderItem={renderSuggestions}
|
||||
ListEmptyComponent={hasPhotos && emptySuggestionsList}
|
||||
/>
|
||||
);
|
||||
|
||||
const displaySearchResults = ( ) => (
|
||||
<FlatList
|
||||
data={list}
|
||||
renderItem={renderSearchResults}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayPhotos = ( ) => currentObs.observationPhotos.map( p => ( {
|
||||
uri: p.photo?.url || p?.photo?.localFilePath
|
||||
} ) );
|
||||
|
||||
return (
|
||||
<ViewNoFooter>
|
||||
<View>
|
||||
{hasPhotos && (
|
||||
<PhotoCarousel
|
||||
photoUris={displayPhotos( )}
|
||||
setSelectedPhotoIndex={setSelectedPhotoIndex}
|
||||
selectedPhotoIndex={selectedPhotoIndex}
|
||||
/>
|
||||
)}
|
||||
<Searchbar
|
||||
placeholder={t( "Tap-to-search-for-taxa" )}
|
||||
onChangeText={setQ}
|
||||
value={q}
|
||||
style={viewStyles.searchBar}
|
||||
/>
|
||||
</View>
|
||||
{list.length > 0 ? displaySearchResults( ) : displaySuggestions( )}
|
||||
<Button
|
||||
level="primary"
|
||||
onPress={toggleSeenNearby}
|
||||
text={showSeenNearby ? "View species not seen nearby" : "View seen nearby"}
|
||||
testID="CVSuggestions.toggleSeenNearby"
|
||||
/>
|
||||
</ViewNoFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default CVSuggestions;
|
||||
@@ -30,7 +30,6 @@ const IdentificationSection = ( ): Node => {
|
||||
const updateIdentification = taxon => updateTaxon( taxon );
|
||||
|
||||
const onIDAdded = async id => {
|
||||
console.log( "onIDAdded", id );
|
||||
updateIdentification( id.taxon );
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ const ObsEdit = ( ): Node => {
|
||||
currentObsIndex,
|
||||
setCurrentObsIndex,
|
||||
observations,
|
||||
openSavedObservation,
|
||||
saveObservation,
|
||||
saveAndUploadObservation,
|
||||
setObservations,
|
||||
@@ -226,6 +227,14 @@ const ObsEdit = ( ): Node => {
|
||||
setPhotoUris( uris );
|
||||
}, [currentObs] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( currentObs ) return;
|
||||
if ( !params?.uuid ) return;
|
||||
|
||||
// This should set the current obs in the context
|
||||
openSavedObservation( params?.uuid );
|
||||
}, [currentObs, openSavedObservation, params?.uuid] );
|
||||
|
||||
const addEvidence = () => {
|
||||
bottomSheetModalRef.current?.present();
|
||||
};
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import InputField from "components/SharedComponents/InputField";
|
||||
import * as React from "react";
|
||||
import {
|
||||
FlatList, Image, Pressable, Text
|
||||
} from "react-native";
|
||||
import useRemoteObsEditSearchResults from "sharedHooks/useRemoteSearchResults";
|
||||
import { imageStyles, viewStyles } from "styles/search/search";
|
||||
|
||||
type Props = {
|
||||
source: string,
|
||||
handlePress: Function
|
||||
}
|
||||
|
||||
const ObsEditSearch = ( {
|
||||
source,
|
||||
handlePress
|
||||
}: Props ): React.Node => {
|
||||
const [q, setQ] = React.useState( "" );
|
||||
// choose users or taxa
|
||||
const list = useRemoteObsEditSearchResults( q, source, "all" );
|
||||
|
||||
// TODO: when UI is finalized, make sure these list results are not duplicate UI
|
||||
// with Search or Projects; share components if possible
|
||||
const renderItem = ( { item } ) => {
|
||||
if ( source === "taxa" ) {
|
||||
const imageUrl = ( item && item.default_photo ) && { uri: item.default_photo.square_url };
|
||||
return (
|
||||
<Pressable
|
||||
onPress={( ) => handlePress( item.id )}
|
||||
style={viewStyles.row}
|
||||
testID={`ObsEditSearch.taxa.${item.id}`}
|
||||
>
|
||||
<Image
|
||||
source={imageUrl}
|
||||
style={imageStyles.squareImage}
|
||||
testID={`ObsEditSearch.taxa.${item.id}.photo`}
|
||||
/>
|
||||
<Text>{`${item.preferred_common_name} (${item.rank} ${item.name})`}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Pressable
|
||||
onPress={( ) => handlePress( item.id )}
|
||||
style={viewStyles.row}
|
||||
testID={`ObsEditSearch.project.${item.id}`}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item.icon }}
|
||||
style={imageStyles.squareImage}
|
||||
testID={`ObsEditSearch.project.${item.id}.photo`}
|
||||
/>
|
||||
<Text>{item.title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputField
|
||||
handleTextChange={setQ}
|
||||
placeholder={source === "taxa" ? "search for taxa" : "search for projects"}
|
||||
text={q}
|
||||
type="none"
|
||||
/>
|
||||
{list.length > 0 && (
|
||||
<FlatList
|
||||
data={list}
|
||||
renderItem={renderItem}
|
||||
testID="ObsEditSearch.listView"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObsEditSearch;
|
||||
@@ -1,93 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { getJWTToken } from "components/LoginSignUp/AuthenticationService";
|
||||
import inatjs, { FileUpload } from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import Photo from "../../../models/Photo";
|
||||
|
||||
const TAXON_FIELDS = {
|
||||
name: true,
|
||||
preferred_common_name: true
|
||||
};
|
||||
|
||||
const PHOTO_FIELDS = {
|
||||
medium_url: true
|
||||
};
|
||||
|
||||
const FIELDS = {
|
||||
taxon: {
|
||||
...TAXON_FIELDS,
|
||||
taxon_photos: {
|
||||
photo: PHOTO_FIELDS
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const useCVSuggestions = (
|
||||
currentObs: Object,
|
||||
showSeenNearby: boolean,
|
||||
selectedPhoto: number
|
||||
): Object => {
|
||||
const [suggestions, setSuggestions] = useState( [] );
|
||||
const [status, setStatus] = useState( null );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( !currentObs || !currentObs.observationPhotos ) { return ( ) => { }; }
|
||||
const uri = currentObs.observationPhotos && currentObs.observationPhotos[selectedPhoto].uri;
|
||||
const { latitude } = currentObs;
|
||||
const { longitude } = currentObs;
|
||||
|
||||
let isCurrent = true;
|
||||
const fetchCVSuggestions = async ( ): Promise<Object> => {
|
||||
try {
|
||||
setSuggestions( [] );
|
||||
// observed_on: new Date( time * 1000 ).toISOString(),
|
||||
const apiToken = await getJWTToken( false );
|
||||
const resizedPhoto = await Photo.resizeImageForUpload( uri );
|
||||
|
||||
const params = {
|
||||
image: new FileUpload( {
|
||||
uri: resizedPhoto,
|
||||
name: "photo.jpeg",
|
||||
type: "image/jpeg"
|
||||
} ),
|
||||
fields: FIELDS
|
||||
};
|
||||
|
||||
if ( showSeenNearby ) {
|
||||
// $FlowFixMe
|
||||
params.latitude = latitude;
|
||||
// $FlowFixMe
|
||||
params.longitude = longitude;
|
||||
}
|
||||
|
||||
const options = {
|
||||
api_token: apiToken
|
||||
};
|
||||
|
||||
const r = await inatjs.computervision.score_image( params, options );
|
||||
if ( r.total_results > 0 ) {
|
||||
setSuggestions( r.results );
|
||||
} else {
|
||||
setStatus( "no_results" );
|
||||
}
|
||||
if ( !isCurrent ) { return; }
|
||||
} catch ( e ) {
|
||||
console.log( JSON.stringify( e.response ), "couldn't fetch CV suggestions" );
|
||||
}
|
||||
};
|
||||
|
||||
fetchCVSuggestions( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [currentObs, showSeenNearby, selectedPhoto] );
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
status
|
||||
};
|
||||
};
|
||||
|
||||
export default useCVSuggestions;
|
||||
@@ -1,21 +1,32 @@
|
||||
// @flow
|
||||
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import ViewWithFooter from "components/SharedComponents/ViewWithFooter";
|
||||
import { fetchProjects } from "api/projects";
|
||||
import ScrollWithFooter from "components/SharedComponents/ScrollWithFooter";
|
||||
import * as React from "react";
|
||||
import { Image, ImageBackground, Text } from "react-native";
|
||||
import {
|
||||
Image, ImageBackground, Text
|
||||
} from "react-native";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { imageStyles, textStyles } from "styles/projects/projectDetails";
|
||||
|
||||
import useProjectDetails from "./hooks/useProjectDetails";
|
||||
import ProjectObservations from "./ProjectObservations";
|
||||
|
||||
const ProjectDetails = ( ): React.Node => {
|
||||
const { params } = useRoute( );
|
||||
const { id } = params;
|
||||
const project = useProjectDetails( id );
|
||||
|
||||
const {
|
||||
data: project
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchProjects", id],
|
||||
optsWithAuth => fetchProjects( id, { }, optsWithAuth )
|
||||
);
|
||||
|
||||
if ( !project ) { return null; }
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<ScrollWithFooter>
|
||||
<ImageBackground
|
||||
source={{ uri: project.header_image_url }}
|
||||
// $FlowFixMe
|
||||
@@ -32,7 +43,7 @@ const ProjectDetails = ( ): React.Node => {
|
||||
<Text style={textStyles.descriptionText}>{project.description}</Text>
|
||||
{/* TODO: support joining or leaving projects once oauth is set up */}
|
||||
<ProjectObservations id={id} />
|
||||
</ViewWithFooter>
|
||||
</ScrollWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { searchObservations } from "api/observations";
|
||||
import GridItem from "components/SharedComponents/ObservationViews/GridItem";
|
||||
import * as React from "react";
|
||||
import { FlatList } from "react-native";
|
||||
|
||||
import useProjectObservations from "./hooks/useProjectObservations";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
|
||||
type Props = {
|
||||
id: number
|
||||
}
|
||||
|
||||
const ProjectObservations = ( { id }: Props ): React.Node => {
|
||||
const observations = useProjectObservations( id );
|
||||
const {
|
||||
data: observations
|
||||
} = useAuthenticatedQuery(
|
||||
["searchObservations", id],
|
||||
optsWithAuth => searchObservations( { project_id: id }, optsWithAuth )
|
||||
);
|
||||
|
||||
const navigation = useNavigation( );
|
||||
const navToObsDetails = observation => {
|
||||
navigation.navigate( "ObsDetails", { uuid: observation.uuid } );
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import fetchSearchResults from "api/search";
|
||||
import PlaceholderText from "components/PlaceholderText";
|
||||
import * as React from "react";
|
||||
import { Pressable } from "react-native";
|
||||
import useRemoteSearchResults from "sharedHooks/useRemoteSearchResults";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
|
||||
import ProjectList from "./ProjectList";
|
||||
|
||||
@@ -13,7 +14,15 @@ type Props = {
|
||||
}
|
||||
|
||||
const ProjectSearch = ( { q, clearSearch }: Props ): React.Node => {
|
||||
const projectSearchResults = useRemoteSearchResults( q, "projects", "all" );
|
||||
const {
|
||||
data: projectSearchResults
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchSearchResults", q],
|
||||
optsWithAuth => fetchSearchResults( {
|
||||
q,
|
||||
sources: "projects"
|
||||
}, optsWithAuth )
|
||||
);
|
||||
|
||||
if ( q === "" ) {
|
||||
return null;
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
// @flow
|
||||
|
||||
import { searchProjects } from "api/projects";
|
||||
import { t } from "i18next";
|
||||
import * as React from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import useUserLocation from "sharedHooks/useUserLocation";
|
||||
import { viewStyles } from "styles/projects/projects";
|
||||
|
||||
import useMemberId from "./hooks/useMemberId";
|
||||
import useProjects from "./hooks/useProjects";
|
||||
import ProjectList from "./ProjectList";
|
||||
|
||||
const ProjectTabs = ( ): React.Node => {
|
||||
const memberId = useMemberId( );
|
||||
type Props = {
|
||||
memberId: number
|
||||
}
|
||||
|
||||
const ProjectTabs = ( { memberId }: Props ): React.Node => {
|
||||
const userJoined = { member_id: memberId };
|
||||
const [apiParams, setApiParams] = React.useState( userJoined );
|
||||
|
||||
const latLng = useUserLocation( );
|
||||
const projects = useProjects( apiParams );
|
||||
|
||||
const {
|
||||
data: projects
|
||||
} = useAuthenticatedQuery(
|
||||
["searchProjects", apiParams],
|
||||
optsWithAuth => searchProjects( apiParams, optsWithAuth )
|
||||
);
|
||||
|
||||
const fetchProjectsByLatLng = ( ) => {
|
||||
setApiParams( {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import { fetchUserMe } from "api/users";
|
||||
import InputField from "components/SharedComponents/InputField";
|
||||
import ViewWithFooter from "components/SharedComponents/ViewWithFooter";
|
||||
import * as React from "react";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
|
||||
import ProjectSearch from "./ProjectSearch";
|
||||
import ProjectTabs from "./ProjectTabs";
|
||||
@@ -12,6 +14,15 @@ const Projects = ( ): React.Node => {
|
||||
|
||||
const clearSearch = ( ) => setQ( "" );
|
||||
|
||||
const {
|
||||
data: user
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchUserMe"],
|
||||
optsWithAuth => fetchUserMe( { }, optsWithAuth )
|
||||
);
|
||||
|
||||
const memberId = user?.id;
|
||||
|
||||
return (
|
||||
<ViewWithFooter testID="Projects">
|
||||
<InputField
|
||||
@@ -24,7 +35,7 @@ const Projects = ( ): React.Node => {
|
||||
{/* TODO: make project search a separate screen or a modal?
|
||||
not sure what the final designs will look like but unlikely
|
||||
tabs and search will both be on the same screen */}
|
||||
<ProjectTabs />
|
||||
{memberId && <ProjectTabs memberId={memberId} />}
|
||||
<ProjectSearch q={q} clearSearch={clearSearch} />
|
||||
</ViewWithFooter>
|
||||
);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { getJWTToken } from "components/LoginSignUp/AuthenticationService";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const useMemberId = ( ): ?number => {
|
||||
const [memberId, setMemberId] = useState( null );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchMemberId = async ( ) => {
|
||||
const apiToken = await getJWTToken( );
|
||||
try {
|
||||
const options = {
|
||||
api_token: apiToken
|
||||
};
|
||||
const { results } = await inatjs.users.me( options );
|
||||
if ( !isCurrent ) { return; }
|
||||
setMemberId( results[0].id );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch current member id:", JSON.stringify( e.response ) );
|
||||
}
|
||||
};
|
||||
|
||||
fetchMemberId( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [] );
|
||||
|
||||
return memberId;
|
||||
};
|
||||
|
||||
export default useMemberId;
|
||||
@@ -1,42 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const FIELDS = {
|
||||
title: true,
|
||||
icon: true,
|
||||
header_image_url: true,
|
||||
description: true
|
||||
};
|
||||
|
||||
const useProjectDetails = ( id: number ): Object => {
|
||||
const [projectDetails, setProjectDetails] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchProjectDetails = async ( ) => {
|
||||
try {
|
||||
const params = {
|
||||
fields: FIELDS
|
||||
};
|
||||
const response = await inatjs.projects.fetch( [id], params );
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setProjectDetails( results[0] );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( `Couldn't fetch project details for project_id ${id}:`, e.message );
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjectDetails( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [id] );
|
||||
|
||||
return projectDetails;
|
||||
};
|
||||
|
||||
export default useProjectDetails;
|
||||
@@ -1,60 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const PHOTO_FIELDS = {
|
||||
id: true,
|
||||
attribution: true,
|
||||
license_code: true,
|
||||
url: true
|
||||
};
|
||||
|
||||
const OBSERVATION_PHOTOS_FIELDS = {
|
||||
id: true,
|
||||
photo: PHOTO_FIELDS,
|
||||
position: true,
|
||||
uuid: true
|
||||
};
|
||||
|
||||
const TAXON_FIELDS = {
|
||||
name: true,
|
||||
preferred_common_name: true
|
||||
};
|
||||
|
||||
const FIELDS = {
|
||||
observation_photos: OBSERVATION_PHOTOS_FIELDS,
|
||||
taxon: TAXON_FIELDS
|
||||
};
|
||||
|
||||
const useProjectObservations = ( id: number ): Object => {
|
||||
const [projectObservations, setProjectObservations] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchProjectObservations = async ( ) => {
|
||||
try {
|
||||
const params = {
|
||||
per_page: 25,
|
||||
fields: FIELDS
|
||||
};
|
||||
const response = await inatjs.observations.search( params );
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setProjectObservations( results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( `Couldn't fetch project observations for project_id ${id}:`, e.message );
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjectObservations( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [id] );
|
||||
|
||||
return projectObservations;
|
||||
};
|
||||
|
||||
export default useProjectObservations;
|
||||
@@ -1,44 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const FIELDS = {
|
||||
title: true,
|
||||
icon: true
|
||||
};
|
||||
|
||||
const useProjects = ( apiParams: Object ): Array<Object> => {
|
||||
const [projects, setProjects] = useState( [] );
|
||||
|
||||
// TODO: check with team on whether this is the best endpoint
|
||||
// for joined projects. otherwise, user/:id/membership?
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchProjects = async ( ) => {
|
||||
try {
|
||||
const params = {
|
||||
per_page: 10,
|
||||
...apiParams,
|
||||
fields: FIELDS
|
||||
};
|
||||
const response = await inatjs.projects.search( params );
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setProjects( results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch projects:", e.message );
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjects( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [apiParams] );
|
||||
|
||||
return projects;
|
||||
};
|
||||
|
||||
export default useProjects;
|
||||
@@ -1,22 +1,32 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import fetchSearchResults from "api/search";
|
||||
import PlaceholderText from "components/PlaceholderText";
|
||||
import InputField from "components/SharedComponents/InputField";
|
||||
import ViewWithFooter from "components/SharedComponents/ViewWithFooter";
|
||||
import * as React from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList, Image, Pressable, Text, View
|
||||
} from "react-native";
|
||||
import useRemoteSearchResults from "sharedHooks/useRemoteSearchResults";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { imageStyles, viewStyles } from "styles/search/search";
|
||||
|
||||
const Search = ( ): React.Node => {
|
||||
const navigation = useNavigation( );
|
||||
const [q, setQ] = React.useState( "" );
|
||||
const [queryType, setQueryType] = React.useState( "taxa" );
|
||||
// choose users or taxa
|
||||
const list = useRemoteSearchResults( q, queryType, "all" );
|
||||
|
||||
const {
|
||||
data, isLoading
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchSearchResults", q],
|
||||
optsWithAuth => fetchSearchResults( {
|
||||
q,
|
||||
sources: queryType
|
||||
}, optsWithAuth )
|
||||
);
|
||||
|
||||
const renderItem = ( { item } ) => {
|
||||
// TODO: make sure TaxonDetails navigates back to Search
|
||||
@@ -84,11 +94,15 @@ const Search = ( ): React.Node => {
|
||||
text={q}
|
||||
type="none"
|
||||
/>
|
||||
<FlatList
|
||||
data={list}
|
||||
renderItem={renderItem}
|
||||
testID="Search.listView"
|
||||
/>
|
||||
{isLoading
|
||||
? <ActivityIndicator />
|
||||
: (
|
||||
<FlatList
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
testID="Search.listView"
|
||||
/>
|
||||
)}
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import fetchPlace from "api/places";
|
||||
import fetchSearchResults from "api/search";
|
||||
import inatPlaceTypes from "dictionaries/places";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Image, Text, TextInput, View
|
||||
} from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { textStyles, viewStyles } from "styles/settings/settings";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
import usePlaceDetails from "./hooks/usePlaceDetails";
|
||||
import usePlaces from "./hooks/usePlaces";
|
||||
|
||||
const PlaceSearchInput = ( { placeId, onPlaceChanged } ): React.Node => {
|
||||
const [hideResults, setHideResults] = React.useState( true );
|
||||
const [placeSearch, setPlaceSearch] = React.useState( "" );
|
||||
// So we'll start searching only once the user finished typing
|
||||
const [finalPlaceSearch] = useDebounce( placeSearch, 500 );
|
||||
const placeResults = usePlaces( finalPlaceSearch );
|
||||
const placeDetails = usePlaceDetails( placeId );
|
||||
|
||||
const queryClient = useQueryClient( );
|
||||
|
||||
// this seems necessary for clearing the cache between searches
|
||||
queryClient.invalidateQueries( ["fetchSearchResults"] );
|
||||
|
||||
const {
|
||||
data: placeResults
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchSearchResults", finalPlaceSearch],
|
||||
optsWithAuth => fetchSearchResults( {
|
||||
q: finalPlaceSearch,
|
||||
sources: "places",
|
||||
fields: "place,place.display_name,place.place_type"
|
||||
}, optsWithAuth )
|
||||
);
|
||||
|
||||
const {
|
||||
data: placeDetails
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchPlace", placeId],
|
||||
optsWithAuth => fetchPlace( placeId, optsWithAuth )
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
if ( placeDetails ) {
|
||||
console.log( "Place details", placeDetails );
|
||||
setPlaceSearch( placeDetails.display_name );
|
||||
} else {
|
||||
setPlaceSearch( "" );
|
||||
@@ -53,7 +74,7 @@ const PlaceSearchInput = ( { placeId, onPlaceChanged } ): React.Node => {
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
{!hideResults && finalPlaceSearch.length > 0 && placeResults.map( place => (
|
||||
{!hideResults && finalPlaceSearch.length > 0 && placeResults?.map( place => (
|
||||
<Pressable
|
||||
key={place.id}
|
||||
style={[viewStyles.row, viewStyles.placeResultContainer]}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { fetchUserMe } from "api/users";
|
||||
import { getAPIToken } from "components/LoginSignUp/AuthenticationService";
|
||||
import ViewWithFooter from "components/SharedComponents/ViewWithFooter";
|
||||
import { t } from "i18next";
|
||||
@@ -16,9 +17,9 @@ import {
|
||||
Text,
|
||||
View
|
||||
} from "react-native";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { textStyles, viewStyles } from "styles/settings/settings";
|
||||
|
||||
import useUserMe from "./hooks/useUserMe";
|
||||
import SettingsAccount from "./SettingsAccount";
|
||||
import SettingsApplications from "./SettingsApplications";
|
||||
import SettingsContentDisplay from "./SettingsContentDisplay";
|
||||
@@ -150,15 +151,19 @@ const Settings = ( { children: _children }: Props ): Node => {
|
||||
const [activeTab, setActiveTab] = useState( TAB_TYPE_PROFILE );
|
||||
const [settings, setSettings] = useState( {} );
|
||||
const [accessToken, setAccessToken] = useState( null );
|
||||
const [isLoading, setIsLoading] = useState( true );
|
||||
const [isSaving, setIsSaving] = useState( false );
|
||||
const user = useUserMe( accessToken );
|
||||
|
||||
const {
|
||||
data: user,
|
||||
isLoading
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchUserMe"],
|
||||
optsWithAuth => fetchUserMe( { }, optsWithAuth )
|
||||
);
|
||||
|
||||
const fetchProfile = useCallback( async () => {
|
||||
if ( user ) {
|
||||
console.log( "User object", user );
|
||||
setSettings( user );
|
||||
setIsLoading( false );
|
||||
}
|
||||
}, [user] );
|
||||
|
||||
@@ -213,7 +218,6 @@ const Settings = ( { children: _children }: Props ): Node => {
|
||||
|
||||
console.log( "Updated user", response );
|
||||
const userResponse = await inatjs.users.me( { api_token: accessToken, fields: "all" } );
|
||||
console.log( "User object", userResponse.results[0] );
|
||||
setSettings( userResponse.results[0] );
|
||||
setIsSaving( false );
|
||||
};
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
// @flow
|
||||
|
||||
import fetchAuthorizedApplications from "api/authorizedApplications";
|
||||
import fetchProviderAuthorizations from "api/providerAuthorizations";
|
||||
import inatProviders from "dictionaries/providers";
|
||||
import { t } from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { Alert, Text, View } from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { textStyles, viewStyles } from "styles/settings/settings";
|
||||
|
||||
import useAuthorizedApplications from "./hooks/useAuthorizedApplications";
|
||||
import useProviderAuthorizations from "./hooks/useProviderAuthorizations";
|
||||
|
||||
type Props = {
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
const SettingsApplications = ( { accessToken }: Props ): Node => {
|
||||
const currentAuthorizedApps = useAuthorizedApplications( accessToken );
|
||||
const [authorizedApps, setAuthorizedApps] = useState( [] );
|
||||
const providerAuthorizations = useProviderAuthorizations( accessToken );
|
||||
const {
|
||||
data: authorizedApps
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchAuthorizedApplications"],
|
||||
optsWithAuth => fetchAuthorizedApplications( { }, optsWithAuth )
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
setAuthorizedApps( currentAuthorizedApps );
|
||||
}, [currentAuthorizedApps] );
|
||||
const {
|
||||
data: providerAuthorizations
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchProviderAuthorizations"],
|
||||
optsWithAuth => fetchProviderAuthorizations( { }, optsWithAuth )
|
||||
);
|
||||
|
||||
const revokeApp = async appId => {
|
||||
const response = await inatjs.authorized_applications.delete(
|
||||
@@ -34,7 +40,6 @@ const SettingsApplications = ( { accessToken }: Props ): Node => {
|
||||
// Refresh authorized applications
|
||||
const apps = await inatjs.authorized_applications.search( {}, { api_token: accessToken } );
|
||||
console.log( "Authorized Applications", apps.results );
|
||||
setAuthorizedApps( apps.results );
|
||||
};
|
||||
|
||||
const askToRevokeApp = app => {
|
||||
@@ -53,7 +58,7 @@ const SettingsApplications = ( { accessToken }: Props ): Node => {
|
||||
return (
|
||||
<View style={viewStyles.column}>
|
||||
<Text style={textStyles.title}>{t( "iNaturalist-Applications" )}</Text>
|
||||
{authorizedApps.filter( app => app.application.official ).map( app => (
|
||||
{authorizedApps?.filter( app => app.application.official ).map( app => (
|
||||
<Text key={app.application.id}>
|
||||
{t( "authorized-on-date", { appName: app.application.name, date: app.created_at } )}
|
||||
</Text>
|
||||
@@ -61,7 +66,7 @@ const SettingsApplications = ( { accessToken }: Props ): Node => {
|
||||
|
||||
<Text style={[textStyles.title, textStyles.marginTop]}>{t( "Connected-Accounts" )}</Text>
|
||||
{Object.keys( inatProviders ).map( providerKey => {
|
||||
const connectedProvider = providerAuthorizations.find(
|
||||
const connectedProvider = providerAuthorizations?.find(
|
||||
x => x.provider_name === providerKey
|
||||
);
|
||||
return (
|
||||
@@ -76,7 +81,7 @@ const SettingsApplications = ( { accessToken }: Props ): Node => {
|
||||
} )}
|
||||
|
||||
<Text style={[textStyles.title, textStyles.marginTop]}>{t( "External-Applications" )}</Text>
|
||||
{authorizedApps.filter( app => !app.application.official ).map( app => (
|
||||
{authorizedApps?.filter( app => !app.application.official ).map( app => (
|
||||
<View key={app.application.id} style={[viewStyles.row, viewStyles.applicationRow]}>
|
||||
<Text style={textStyles.applicationName}>
|
||||
{t( "authorized-on-date", { appName: app.application.name, date: app.created_at } )}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import { Picker } from "@react-native-picker/picker";
|
||||
import fetchRelationships from "api/relationships";
|
||||
import { fetchRemoteUsers } from "api/users";
|
||||
import { t } from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
Alert, Image, Text, TextInput, View
|
||||
Alert, Image, ScrollView,
|
||||
Text, TextInput, View
|
||||
} from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import colors from "styles/colors";
|
||||
import { textStyles, viewStyles } from "styles/settings/settings";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
import BlockedUser from "./BlockedUser";
|
||||
import useRelationships from "./hooks/useRelationships";
|
||||
import MutedUser from "./MutedUser";
|
||||
import Relationship from "./Relationship";
|
||||
import UserSearchInput from "./UserSearchInput";
|
||||
@@ -50,10 +53,8 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
const [finalUserSearch] = useDebounce( userSearch, 500 );
|
||||
const [following, setFollowing] = React.useState( "all" );
|
||||
const [trusted, setTrusted] = React.useState( "all" );
|
||||
const [sortBy, setSortBy] = React.useState( "desc" );
|
||||
const [sortBy, setSortBy] = React.useState( "z_to_a" );
|
||||
const [page, setPage] = React.useState( 1 );
|
||||
const [blockedUsers, setBlockedUsers] = React.useState( [] );
|
||||
const [mutedUsers, setMutedUsers] = React.useState( [] );
|
||||
|
||||
const [refreshRelationships, setRefreshRelationships] = React.useState( Math.random() );
|
||||
let orderBy;
|
||||
@@ -77,71 +78,37 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
trusted,
|
||||
order_by: orderBy,
|
||||
order,
|
||||
per_page: 10,
|
||||
page,
|
||||
random: refreshRelationships
|
||||
};
|
||||
const [
|
||||
relationshipResults,
|
||||
perPage,
|
||||
totalResults
|
||||
] = useRelationships( accessToken, relationshipParams );
|
||||
|
||||
const {
|
||||
data
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchRelationships", finalUserSearch],
|
||||
optsWithAuth => fetchRelationships( relationshipParams, optsWithAuth )
|
||||
);
|
||||
|
||||
const {
|
||||
data: blockedUsers
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchRemoteUsers", settings.blocked_user_ids],
|
||||
optsWithAuth => fetchRemoteUsers( settings.blocked_user_ids, { }, optsWithAuth )
|
||||
);
|
||||
|
||||
const {
|
||||
data: mutedUsers
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchRemoteUsers", settings.muted_user_ids],
|
||||
optsWithAuth => fetchRemoteUsers( settings.muted_user_ids, { }, optsWithAuth )
|
||||
);
|
||||
|
||||
const relationshipResults = data?.results;
|
||||
const perPage = data?.per_page;
|
||||
const totalResults = data?.total_results;
|
||||
|
||||
const totalPages = totalResults > 0 && perPage > 0 ? Math.ceil( totalResults / perPage ) : 1;
|
||||
|
||||
useEffect( () => {
|
||||
const getBlockedUsers = async () => {
|
||||
try {
|
||||
const responses = await Promise.all(
|
||||
settings.blocked_user_ids.map(
|
||||
userId => inatjs.users.fetch( userId, { fields: "icon,login,name" } )
|
||||
)
|
||||
);
|
||||
setBlockedUsers( responses.map( r => r.results[0] ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Couldn't retrieve blocked users!",
|
||||
[{ text: "OK" }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
if ( settings.blocked_user_ids.length > 0 ) {
|
||||
getBlockedUsers();
|
||||
} else {
|
||||
setBlockedUsers( [] );
|
||||
}
|
||||
|
||||
const getMutedUsers = async () => {
|
||||
try {
|
||||
const responses = await Promise.all(
|
||||
settings.muted_user_ids.map(
|
||||
userId => inatjs.users.fetch( userId, { fields: "icon,login,name" } )
|
||||
)
|
||||
);
|
||||
setMutedUsers( responses.map( r => r.results[0] ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Couldn't retrieve muted users!",
|
||||
[{ text: "OK" }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
if ( settings.muted_user_ids.length > 0 ) {
|
||||
getMutedUsers();
|
||||
} else {
|
||||
setMutedUsers( [] );
|
||||
}
|
||||
}, [settings] );
|
||||
|
||||
const updateRelationship = useCallback( async ( relationship, update ) => {
|
||||
let response;
|
||||
try {
|
||||
@@ -300,7 +267,7 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
|
||||
return (
|
||||
// $FlowFixMe
|
||||
<View style={viewStyles.column}>
|
||||
<ScrollView contentContainerStyle={viewStyles.column}>
|
||||
<Text style={textStyles.title}>{t( "Relationships" )}</Text>
|
||||
<View style={viewStyles.row}>
|
||||
<TextInput
|
||||
@@ -383,7 +350,7 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{relationshipResults.map( relationship => (
|
||||
{relationshipResults?.map( relationship => (
|
||||
<Relationship
|
||||
key={relationship.id}
|
||||
relationship={relationship}
|
||||
@@ -421,16 +388,16 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
|
||||
<Text style={textStyles.title}>{t( "Blocked-Users" )}</Text>
|
||||
<UserSearchInput userId={0} onUserChanged={u => blockUser( u )} />
|
||||
{blockedUsers.map( user => (
|
||||
{blockedUsers?.map( user => (
|
||||
<BlockedUser key={user.id} user={user} unblockUser={unblockUser} />
|
||||
) )}
|
||||
|
||||
<Text style={textStyles.title}>{t( "Muted-Users" )}</Text>
|
||||
<UserSearchInput userId={0} onUserChanged={u => muteUser( u )} />
|
||||
{mutedUsers.map( user => (
|
||||
{mutedUsers?.map( user => (
|
||||
<MutedUser key={user.id} user={user} unmuteUser={unmuteUser} />
|
||||
) )}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import fetchSearchResults from "api/search";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Image, Text, TextInput, View
|
||||
} from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import useRemoteSearchResults from "sharedHooks/useRemoteSearchResults";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { textStyles, viewStyles } from "styles/settings/settings";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
@@ -12,11 +13,16 @@ const UserSearchInput = ( { onUserChanged } ): React.Node => {
|
||||
const [userSearch, setUserSearch] = React.useState( "" );
|
||||
// So we'll start searching only once the user finished typing
|
||||
const [finalUserSearch] = useDebounce( userSearch, 500 );
|
||||
const userResults = useRemoteSearchResults(
|
||||
finalUserSearch,
|
||||
"users",
|
||||
"user.login,user.name,user.icon"
|
||||
).map( r => r.user );
|
||||
const {
|
||||
data: userResults
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchSearchResults", finalUserSearch],
|
||||
optsWithAuth => fetchSearchResults( {
|
||||
q: finalUserSearch,
|
||||
sources: "users",
|
||||
fields: "user.login,user.name,user.icon"
|
||||
}, optsWithAuth )
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
if ( finalUserSearch.length === 0 ) {
|
||||
@@ -50,7 +56,7 @@ const UserSearchInput = ( { onUserChanged } ): React.Node => {
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
{!hideResults && finalUserSearch.length > 0 && userResults.map( result => (
|
||||
{!hideResults && finalUserSearch.length > 0 && userResults?.map( result => (
|
||||
<Pressable
|
||||
key={result.id}
|
||||
style={[viewStyles.row, viewStyles.placeResultContainer]}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
const useAuthorizedApplications = ( accessToken: string ): Array<Object> => {
|
||||
const [searchResults, setSearchResults] = useState( [] );
|
||||
|
||||
useEffect( () => {
|
||||
let isCurrent = true;
|
||||
const cleanUp = ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
const fetchSearchResults = async () => {
|
||||
try {
|
||||
const response = await inatjs.authorized_applications.search(
|
||||
{ fields: "application.official,application.name,created_at" },
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) {
|
||||
return;
|
||||
}
|
||||
setSearchResults( results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) {
|
||||
return;
|
||||
}
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Couldn't retrieve authorized applications!",
|
||||
[{ text: "OK" }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// don't bother to fetch search results if there isn't a query
|
||||
if ( !accessToken ) {
|
||||
return cleanUp;
|
||||
}
|
||||
fetchSearchResults();
|
||||
return cleanUp;
|
||||
}, [accessToken] );
|
||||
|
||||
return searchResults;
|
||||
};
|
||||
|
||||
export default useAuthorizedApplications;
|
||||
@@ -1,42 +0,0 @@
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
const usePlaceDetails = ( placeId: string ): Array<Object> => {
|
||||
const [searchResult, setSearchResult] = useState( null );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const cleanUp = ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
const fetchSearchResults = async ( ) => {
|
||||
try {
|
||||
const response = await inatjs.places.fetch( placeId, { fields: "display_name" } );
|
||||
const result = response.results[0];
|
||||
if ( !isCurrent ) { return; }
|
||||
setSearchResult( result );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Couldn't retrieve place details!",
|
||||
[{ text: "OK" }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// don't bother to fetch search results if there isn't a query
|
||||
if ( !placeId ) { return cleanUp; }
|
||||
fetchSearchResults( );
|
||||
return cleanUp;
|
||||
}, [placeId] );
|
||||
|
||||
return searchResult;
|
||||
};
|
||||
|
||||
export default usePlaceDetails;
|
||||
@@ -1,48 +0,0 @@
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
const usePlaces = ( q: string ): Array<Object> => {
|
||||
const [searchResults, setSearchResults] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const cleanUp = ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
const fetchSearchResults = async ( ) => {
|
||||
try {
|
||||
const params = {
|
||||
per_page: 10,
|
||||
q,
|
||||
sources: "places",
|
||||
fields: "place,place.display_name,place.place_type"
|
||||
};
|
||||
const response = await inatjs.search( params );
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setSearchResults( results.map( r => r.place ) );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Couldn't retrieve places!",
|
||||
[{ text: "OK" }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// don't bother to fetch search results if there isn't a query
|
||||
if ( q === "" ) { return cleanUp; }
|
||||
fetchSearchResults( );
|
||||
return cleanUp;
|
||||
}, [q] );
|
||||
|
||||
return searchResults;
|
||||
};
|
||||
|
||||
export default usePlaces;
|
||||
@@ -1,51 +0,0 @@
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
const useProviderAuthorizations = ( accessToken: string ): Array<Object> => {
|
||||
const [searchResults, setSearchResults] = useState( [] );
|
||||
|
||||
useEffect( () => {
|
||||
let isCurrent = true;
|
||||
const cleanUp = () => {
|
||||
isCurrent = false;
|
||||
};
|
||||
const fetchSearchResults = async () => {
|
||||
try {
|
||||
const response = await inatjs.provider_authorizations.search(
|
||||
{ fields: "provider_name,created_at" },
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) {
|
||||
return;
|
||||
}
|
||||
setSearchResults( results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) {
|
||||
return;
|
||||
}
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Couldn't retrieve provider authorizations!",
|
||||
[{ text: "OK" }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// don't bother to fetch search results if there isn't a query
|
||||
if ( !accessToken ) {
|
||||
return cleanUp;
|
||||
}
|
||||
fetchSearchResults();
|
||||
return cleanUp;
|
||||
}, [accessToken] );
|
||||
|
||||
return searchResults;
|
||||
};
|
||||
|
||||
export default useProviderAuthorizations;
|
||||
@@ -1,74 +0,0 @@
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
const useRelationships = ( accessToken, {
|
||||
following,
|
||||
order,
|
||||
order_by, // eslint-disable-line camelcase
|
||||
page,
|
||||
per_page, // eslint-disable-line camelcase
|
||||
q,
|
||||
random,
|
||||
trusted
|
||||
} ): Array<Object> => {
|
||||
const [searchResults, setSearchResults] = useState( [] );
|
||||
const [perPage, setPerPage] = useState( 0 );
|
||||
const [totalResults, setTotalResults] = useState( 0 );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const cleanUp = ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
const fetchSearchResults = async ( ) => {
|
||||
try {
|
||||
const response = await inatjs.relationships.search( {
|
||||
q,
|
||||
following,
|
||||
trusted,
|
||||
order_by,
|
||||
order,
|
||||
per_page,
|
||||
page,
|
||||
fields: "all"
|
||||
}, { api_token: accessToken } );
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setSearchResults( results );
|
||||
setPerPage( response.per_page );
|
||||
setTotalResults( response.total_results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Couldn't retrieve relationships!",
|
||||
[{ text: "OK" }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// don't bother to fetch search results if there isn't a query
|
||||
if ( !accessToken ) { return cleanUp; }
|
||||
fetchSearchResults( );
|
||||
return cleanUp;
|
||||
}, [
|
||||
accessToken,
|
||||
following,
|
||||
order,
|
||||
order_by, // eslint-disable-line camelcase
|
||||
page,
|
||||
per_page, // eslint-disable-line camelcase
|
||||
q,
|
||||
random,
|
||||
trusted
|
||||
] );
|
||||
|
||||
return [searchResults, perPage, totalResults];
|
||||
};
|
||||
|
||||
export default useRelationships;
|
||||
@@ -1,47 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
const useUserMe = ( accessToken: string ): Array<Object> | null => {
|
||||
const [result, setResult] = useState( null );
|
||||
|
||||
useEffect( ( ): function => {
|
||||
let isCurrent = true;
|
||||
const cleanUp = ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
const fetchSearchResults = async ( ) => {
|
||||
try {
|
||||
const response = await inatjs.users.me( {
|
||||
api_token: accessToken,
|
||||
fields:
|
||||
"all"
|
||||
} );
|
||||
if ( !isCurrent ) { return; }
|
||||
setResult( response.results[0] );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Couldn't retrieve user details!",
|
||||
[{ text: "OK" }],
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// don't bother to fetch search results if there isn't a query
|
||||
if ( !accessToken ) { return cleanUp; }
|
||||
fetchSearchResults( );
|
||||
return cleanUp;
|
||||
}, [accessToken] );
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default useUserMe;
|
||||
@@ -1,21 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text } from "react-native";
|
||||
import { textStyles, viewStyles } from "styles/sharedComponents/customHeader";
|
||||
|
||||
type Props = {
|
||||
headerText: string
|
||||
}
|
||||
|
||||
const CustomHeaderWithTranslation = ( { headerText }: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
|
||||
return (
|
||||
<Text style={[viewStyles.element, textStyles.text]}>{t( headerText )}</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomHeaderWithTranslation;
|
||||
@@ -108,7 +108,7 @@ const ObservationViews = ( {
|
||||
);
|
||||
|
||||
const navToObsDetails = async observation => {
|
||||
navigation.navigate( "ObsDetails", { observation } );
|
||||
navigation.navigate( "ObsDetails", { uuid: observation.uuid } );
|
||||
};
|
||||
|
||||
const renderItem = ( { item } ) => (
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { fetchRemoteUser } from "api/users";
|
||||
import TranslatedText from "components/SharedComponents/TranslatedText";
|
||||
import UserIcon from "components/SharedComponents/UserIcon";
|
||||
import useRemoteUser from "components/UserProfile/hooks/useRemoteUser";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import IconMaterial from "react-native-vector-icons/MaterialIcons";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import useCurrentUser from "sharedHooks/useCurrentUser";
|
||||
import colors from "styles/colors";
|
||||
import { textStyles, viewStyles } from "styles/observations/userCard";
|
||||
@@ -16,7 +17,15 @@ import User from "../../../models/User";
|
||||
|
||||
const UserCard = ( ): Node => {
|
||||
const user = useCurrentUser( );
|
||||
const { user: remoteUser } = useRemoteUser( user?.id );
|
||||
const userId = user?.id;
|
||||
|
||||
const {
|
||||
data: remoteUser
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchRemoteUser", userId],
|
||||
optsWithAuth => fetchRemoteUser( userId, { }, optsWithAuth )
|
||||
);
|
||||
|
||||
// TODO: this currently doesn't show up on initial login
|
||||
// because user id can't be fetched
|
||||
const navigation = useNavigation( );
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
// import useNetworkSite from "./hooks/useNetworkSite";
|
||||
import { fetchRemoteUser } from "api/users";
|
||||
import Button from "components/SharedComponents/Buttons/Button";
|
||||
import CustomHeader from "components/SharedComponents/CustomHeader";
|
||||
import UserIcon from "components/SharedComponents/UserIcon";
|
||||
@@ -10,20 +10,26 @@ import { t } from "i18next";
|
||||
import * as React from "react";
|
||||
import { Text, useWindowDimensions, View } from "react-native";
|
||||
import HTML from "react-native-render-html";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
import { textStyles, viewStyles } from "styles/userProfile/userProfile";
|
||||
|
||||
import User from "../../models/User";
|
||||
// import useNetworkSite from "./hooks/useNetworkSite";
|
||||
import updateRelationship from "./helpers/updateRelationship";
|
||||
import useRemoteUser from "./hooks/useRemoteUser";
|
||||
import UserProjects from "./UserProjects";
|
||||
|
||||
const UserProfile = ( ): React.Node => {
|
||||
const { params } = useRoute( );
|
||||
const { userId } = params;
|
||||
const { user, currentUser } = useRemoteUser( userId );
|
||||
const { width } = useWindowDimensions( );
|
||||
// const site = useNetworkSite( );
|
||||
|
||||
const {
|
||||
data: remoteUser
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchRemoteUser", userId],
|
||||
optsWithAuth => fetchRemoteUser( userId, { }, optsWithAuth )
|
||||
);
|
||||
|
||||
const user = remoteUser ? remoteUser[0] : null;
|
||||
|
||||
const showCount = ( count, label ) => (
|
||||
<View style={viewStyles.countBox}>
|
||||
@@ -34,7 +40,7 @@ const UserProfile = ( ): React.Node => {
|
||||
|
||||
if ( !user ) { return null; }
|
||||
|
||||
const showUserRole = user.roles.length > 0 && <Text>{`iNaturalist ${user.roles[0]}`}</Text>;
|
||||
const showUserRole = user?.roles?.length > 0 && <Text>{`iNaturalist ${user.roles[0]}`}</Text>;
|
||||
|
||||
const followUser = ( ) => updateRelationship( { id: userId, relationship: { following: true } } );
|
||||
|
||||
@@ -51,26 +57,25 @@ const UserProfile = ( ): React.Node => {
|
||||
<Text>{`${t( "Affiliation-colon" )} ${user.site_id}`}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{!currentUser && (
|
||||
<View style={viewStyles.buttonRow}>
|
||||
<View style={viewStyles.button}>
|
||||
<Button
|
||||
level="primary"
|
||||
text="Follow"
|
||||
onPress={followUser}
|
||||
testID="UserProfile.followButton"
|
||||
/>
|
||||
</View>
|
||||
<View style={viewStyles.button}>
|
||||
<Button
|
||||
level="primary"
|
||||
text="Messages"
|
||||
onPress={( ) => console.log( "open messages" )}
|
||||
testID="UserProfile.messagesButton"
|
||||
/>
|
||||
</View>
|
||||
{/* TODO: hide follow and messages for current user */}
|
||||
<View style={viewStyles.buttonRow}>
|
||||
<View style={viewStyles.button}>
|
||||
<Button
|
||||
level="primary"
|
||||
text="Follow"
|
||||
onPress={followUser}
|
||||
testID="UserProfile.followButton"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<View style={viewStyles.button}>
|
||||
<Button
|
||||
level="primary"
|
||||
text="Messages"
|
||||
onPress={( ) => console.log( "open messages" )}
|
||||
testID="UserProfile.messagesButton"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={viewStyles.countRow}>
|
||||
{showCount( user.observations_count, t( "Observations" ) )}
|
||||
{showCount( user.species_count, t( "Species" ) )}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import { fetchMemberProjects } from "api/users";
|
||||
import ProjectList from "components/Projects/ProjectList";
|
||||
import * as React from "react";
|
||||
|
||||
import useMemberProjects from "./hooks/useMemberProjects";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
|
||||
type Props = {
|
||||
userId: number
|
||||
}
|
||||
|
||||
const UserProjects = ( { userId }: Props ): React.Node => {
|
||||
const projects = useMemberProjects( userId );
|
||||
const {
|
||||
data: projects
|
||||
} = useAuthenticatedQuery(
|
||||
["fetchMemberProjects", userId],
|
||||
optsWithAuth => fetchMemberProjects( { id: userId }, optsWithAuth )
|
||||
);
|
||||
|
||||
return (
|
||||
<ProjectList data={projects} />
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const FIELDS = {
|
||||
title: true,
|
||||
icon: true
|
||||
};
|
||||
|
||||
const useMemberProjects = ( userId: number ): Array<Object> => {
|
||||
const [projects, setProjects] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchMemberProjects = async ( ) => {
|
||||
try {
|
||||
const params = {
|
||||
per_page: 10,
|
||||
id: userId,
|
||||
fields: FIELDS
|
||||
};
|
||||
const response = await inatjs.users.projects( params );
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setProjects( results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch member projects:", e.message );
|
||||
}
|
||||
};
|
||||
|
||||
fetchMemberProjects( );
|
||||
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [userId] );
|
||||
|
||||
return projects;
|
||||
};
|
||||
|
||||
export default useMemberProjects;
|
||||
@@ -1,43 +0,0 @@
|
||||
// // @flow
|
||||
|
||||
// import { useEffect, useState } from "react";
|
||||
// import inatjs from "inaturalistjs";
|
||||
|
||||
// const FIELDS = {
|
||||
// title: true,
|
||||
// icon: true
|
||||
// };
|
||||
|
||||
// const useNetworkSite = ( ): Array<Object> => {
|
||||
// // const [projects, setProjects] = useState( [] );
|
||||
|
||||
// useEffect( ( ) => {
|
||||
// let isCurrent = true;
|
||||
// const fetchSite = async ( ) => {
|
||||
// try {
|
||||
// // const params = {
|
||||
// // per_page: 10,
|
||||
// // id: userId,
|
||||
// // fields: FIELDS
|
||||
// // };
|
||||
// const response = await inatjs.sites.fetch( );
|
||||
// const { results } = response;
|
||||
// console.log( response, "response sites" );
|
||||
// if ( !isCurrent ) { return; }
|
||||
// } catch ( e ) {
|
||||
// if ( !isCurrent ) { return; }
|
||||
// console.log( "Couldn't fetch network sites:", e.message, );
|
||||
// }
|
||||
// };
|
||||
|
||||
// fetchSite( );
|
||||
|
||||
// return ( ) => {
|
||||
// isCurrent = false;
|
||||
// };
|
||||
// }, [] );
|
||||
|
||||
// return [];
|
||||
// };
|
||||
|
||||
// export default useNetworkSite;
|
||||
@@ -1,56 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const USER_FIELDS = {
|
||||
name: true,
|
||||
login: true,
|
||||
icon_url: true,
|
||||
created_at: true,
|
||||
roles: true,
|
||||
site_id: true,
|
||||
description: true,
|
||||
updated_at: true,
|
||||
species_count: true,
|
||||
observations_count: true,
|
||||
identifications_count: true,
|
||||
journal_posts_count: true,
|
||||
site: true
|
||||
};
|
||||
|
||||
const useRemoteUser = ( userId: number ): Object => {
|
||||
const [user, setUser] = useState( null );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchUserProfile = async ( ) => {
|
||||
if ( !userId ) {
|
||||
setUser( null );
|
||||
return;
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await inatjs.users.fetch( userId, { fields: USER_FIELDS } );
|
||||
} catch ( e ) {
|
||||
console.log( "Failed to fetch current user: ", JSON.stringify( e.response ) );
|
||||
setUser( null );
|
||||
return;
|
||||
}
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setUser( results[0] );
|
||||
};
|
||||
|
||||
fetchUserProfile( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [userId] );
|
||||
|
||||
return {
|
||||
user
|
||||
};
|
||||
};
|
||||
|
||||
export default useRemoteUser;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Realm } from "@realm/react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import Realm from "realm";
|
||||
|
||||
import User from "./User";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Realm from "realm";
|
||||
import { Realm } from "@realm/react";
|
||||
|
||||
import Taxon from "./Taxon";
|
||||
import User from "./User";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Realm } from "@realm/react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import uuid from "react-native-uuid";
|
||||
import Realm from "realm";
|
||||
import { createObservedOnStringForUpload, formatDateAndTime } from "sharedHelpers/dateAndTime";
|
||||
|
||||
import Comment from "./Comment";
|
||||
@@ -10,9 +10,6 @@ import ObservationSound from "./ObservationSound";
|
||||
import Taxon from "./Taxon";
|
||||
import User from "./User";
|
||||
|
||||
// noting that methods like .toJSON( ) are only accessible when the model
|
||||
// class is extended with Realm.Object per this issue:
|
||||
// https://github.com/realm/realm-js/issues/3600#issuecomment-785828614
|
||||
class Observation extends Realm.Object {
|
||||
static FIELDS = {
|
||||
captive: true,
|
||||
@@ -189,8 +186,8 @@ class Observation extends Realm.Object {
|
||||
const observationSounds = addTimestampsToEvidence( obs.observationSounds );
|
||||
|
||||
const obsToSave = {
|
||||
// just ...obs causes problems when obs is a realm object
|
||||
...obs.toJSON( ),
|
||||
// causes problems without toJSON when obs is a realm object
|
||||
...( obs.toJSON ? obs.toJSON( ) : obs ),
|
||||
...timestamps,
|
||||
taxon,
|
||||
observationPhotos,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Realm } from "@realm/react";
|
||||
import { FileUpload } from "inaturalistjs";
|
||||
import uuid from "react-native-uuid";
|
||||
import Realm from "realm";
|
||||
|
||||
import Photo from "./Photo";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Realm } from "@realm/react";
|
||||
import { FileUpload } from "inaturalistjs";
|
||||
import { Platform } from "react-native";
|
||||
import RNFS from "react-native-fs";
|
||||
import uuid from "react-native-uuid";
|
||||
import Realm from "realm";
|
||||
|
||||
class ObservationSound extends Realm.Object {
|
||||
static async moveFromCacheToDocumentDirectory( soundUUID ) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createResizedImage } from "@bam.tech/react-native-image-resizer";
|
||||
import { Realm } from "@realm/react";
|
||||
import { Platform } from "react-native";
|
||||
import RNFS from "react-native-fs";
|
||||
import Realm from "realm";
|
||||
|
||||
class Photo extends Realm.Object {
|
||||
static PHOTO_FIELDS = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Realm from "realm";
|
||||
import { Realm } from "@realm/react";
|
||||
|
||||
import Photo from "./Photo";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Realm from "realm";
|
||||
import { Realm } from "@realm/react";
|
||||
|
||||
class User extends Realm.Object {
|
||||
static USER_FIELDS = {
|
||||
|
||||
@@ -9,13 +9,10 @@ import ExploreLanding from "components/Explore/ExploreLanding";
|
||||
import Messages from "components/Messages/Messages";
|
||||
import ObsDetails from "components/ObsDetails/ObsDetails";
|
||||
import AddID from "components/ObsEdit/AddID";
|
||||
import CVSuggestions from "components/ObsEdit/CVSuggestions";
|
||||
import ObsEdit from "components/ObsEdit/ObsEdit";
|
||||
import ObsList from "components/Observations/ObsList";
|
||||
import GroupPhotos from "components/PhotoLibrary/GroupPhotos";
|
||||
import PhotoGallery from "components/PhotoLibrary/PhotoGallery";
|
||||
import CustomHeaderWithTranslation from
|
||||
"components/SharedComponents/CustomHeaderWithTranslation";
|
||||
import Mortal from "components/SharedComponents/Mortal";
|
||||
import PermissionGate from "components/SharedComponents/PermissionGate";
|
||||
import SoundRecorder from "components/SoundRecorder/SoundRecorder";
|
||||
@@ -120,14 +117,6 @@ const MainStackNavigation = ( ): React.Node => (
|
||||
name="StandardCamera"
|
||||
component={StandardCameraWithPermission}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Suggestions"
|
||||
component={CVSuggestions}
|
||||
options={{
|
||||
headerTitle: <CustomHeaderWithTranslation headerText="IDENTIFICATION" />,
|
||||
headerShown: true
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AddID"
|
||||
component={AddID}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
import inatjs from "inaturalistjs";
|
||||
import { searchObservations } from "api/observations";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import useAuthenticatedQuery from "sharedHooks/useAuthenticatedQuery";
|
||||
|
||||
import Observation from "../models/Observation";
|
||||
import { ExploreContext } from "./contexts";
|
||||
@@ -35,7 +36,6 @@ const initialFilters = {
|
||||
};
|
||||
|
||||
const ExploreProvider = ( { children }: Props ): Node => {
|
||||
const [exploreList, setExploreList] = useState( [] );
|
||||
const [exploreFilters, setExploreFilters] = useState( {
|
||||
...initialOptions,
|
||||
...initialFilters
|
||||
@@ -43,48 +43,26 @@ const ExploreProvider = ( { children }: Props ): Node => {
|
||||
const [unappliedFilters, setUnappliedFilters] = useState( {
|
||||
...initialFilters
|
||||
} );
|
||||
const [loadingExplore, setLoadingExplore] = useState( false );
|
||||
const [taxon, setTaxon] = useState( "" );
|
||||
const [location, setLocation] = useState( "" );
|
||||
const [totalObservations, setTotalObservations] = useState( null );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
// create filters object excluding keys with null values
|
||||
const filters = Object.fromEntries(
|
||||
Object.entries( exploreFilters ).filter( ( [_, v] ) => v != null )
|
||||
);
|
||||
|
||||
if ( !loadingExplore ) { return ( ) => { }; }
|
||||
const searchParams = {
|
||||
...filters,
|
||||
fields: Observation.FIELDS
|
||||
};
|
||||
|
||||
const fetchExplore = async ( ) => {
|
||||
// create filters object excluding keys with null values
|
||||
const filters = Object.fromEntries(
|
||||
Object.entries( exploreFilters ).filter( ( [_, v] ) => v != null )
|
||||
);
|
||||
try {
|
||||
const params = {
|
||||
...filters,
|
||||
fields: Observation.FIELDS
|
||||
};
|
||||
const response = await inatjs.observations.search( params );
|
||||
const totalResults = response.total_results;
|
||||
const { results } = await response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setExploreList( results.map( obs => Observation.mimicRealmMappedPropertiesSchema( obs ) ) );
|
||||
setLoadingExplore( false );
|
||||
setTotalObservations( totalResults );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
setLoadingExplore( false );
|
||||
console.log( "Couldn't fetch explore observations:", e.message );
|
||||
}
|
||||
};
|
||||
|
||||
fetchExplore( );
|
||||
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [exploreFilters, loadingExplore] );
|
||||
|
||||
const setLoading = ( ) => setLoadingExplore( true );
|
||||
const {
|
||||
data: exploreList,
|
||||
isLoading: loadingExplore
|
||||
} = useAuthenticatedQuery(
|
||||
["searchObservations"],
|
||||
optsWithAuth => searchObservations( searchParams, optsWithAuth )
|
||||
);
|
||||
|
||||
const resetUnappliedFilters = ( ) => setUnappliedFilters( {
|
||||
...initialFilters
|
||||
@@ -97,9 +75,7 @@ const ExploreProvider = ( { children }: Props ): Node => {
|
||||
} );
|
||||
|
||||
const applyFilters = ( ) => {
|
||||
setLoadingExplore( true );
|
||||
const applied = Object.assign( exploreFilters, unappliedFilters );
|
||||
console.log( applied, "applied" );
|
||||
setExploreFilters( applied );
|
||||
};
|
||||
return {
|
||||
@@ -111,12 +87,10 @@ const ExploreProvider = ( { children }: Props ): Node => {
|
||||
resetFilters,
|
||||
resetUnappliedFilters,
|
||||
setExploreFilters,
|
||||
setLoading,
|
||||
setLocation,
|
||||
setTaxon,
|
||||
setUnappliedFilters,
|
||||
taxon,
|
||||
totalObservations,
|
||||
unappliedFilters
|
||||
};
|
||||
}, [
|
||||
@@ -125,7 +99,6 @@ const ExploreProvider = ( { children }: Props ): Node => {
|
||||
loadingExplore,
|
||||
location,
|
||||
taxon,
|
||||
totalObservations,
|
||||
unappliedFilters
|
||||
] );
|
||||
|
||||
|
||||
@@ -46,30 +46,31 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
};
|
||||
|
||||
const updateObservationKey = ( key, value ) => {
|
||||
const updatedObs = observations.map( ( obs, index ) => {
|
||||
const updatedObservations = observations.map( ( obs, index ) => {
|
||||
if ( index === currentObsIndex ) {
|
||||
return {
|
||||
...obs,
|
||||
...( obs.toJSON ? obs.toJSON( ) : obs ),
|
||||
// $FlowFixMe
|
||||
[key]: value
|
||||
};
|
||||
}
|
||||
return obs;
|
||||
} );
|
||||
setObservations( updatedObs );
|
||||
setObservations( updatedObservations );
|
||||
};
|
||||
|
||||
const updateObservationKeys = keysAndValues => {
|
||||
const updatedObs = observations.map( ( obs, index ) => {
|
||||
const updatedObservations = observations.map( ( obs, index ) => {
|
||||
if ( index === currentObsIndex ) {
|
||||
return {
|
||||
...obs,
|
||||
const updatedObservation = {
|
||||
...( obs.toJSON ? obs.toJSON( ) : obs ),
|
||||
...keysAndValues
|
||||
};
|
||||
return updatedObservation;
|
||||
}
|
||||
return obs;
|
||||
} );
|
||||
setObservations( updatedObs );
|
||||
setObservations( updatedObservations );
|
||||
};
|
||||
|
||||
const updateTaxon = taxon => {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import User from "../models/User";
|
||||
|
||||
export default {
|
||||
subject: true,
|
||||
body: true,
|
||||
from_user: User.USER_FIELDS,
|
||||
to_user: User.USER_FIELDS
|
||||
};
|
||||
24
src/sharedHooks/useAuthenticatedMutation.js
Normal file
24
src/sharedHooks/useAuthenticatedMutation.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { getJWTToken } from "components/LoginSignUp/AuthenticationService";
|
||||
|
||||
// Should work like React Query's useMutation except it calls the queryFunction
|
||||
// with an object that includes the JWT
|
||||
const useAuthenticatedMutation = (
|
||||
queryFunction: Function,
|
||||
handleCallback: Function,
|
||||
queryOptions: Object = {}
|
||||
): any => useMutation( async ( ) => {
|
||||
// Note, getJWTToken() takes care of fetching a new token if the existing
|
||||
// one is expired. We *could* store the token in state with useState if
|
||||
// fetching from RNSInfo becomes a performance issue
|
||||
const apiToken = await getJWTToken( );
|
||||
const options = {
|
||||
...queryOptions,
|
||||
api_token: apiToken
|
||||
};
|
||||
return queryFunction( options );
|
||||
}, handleCallback );
|
||||
|
||||
export default useAuthenticatedMutation;
|
||||
@@ -1,54 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const useRemoteSearchResults = ( q: string, sources: string, fields: string ): Array<Object> => {
|
||||
const [searchResults, setSearchResults] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchSearchResults = async ( ) => {
|
||||
try {
|
||||
const params = {
|
||||
per_page: 10,
|
||||
q,
|
||||
sources,
|
||||
fields: fields || "all"
|
||||
};
|
||||
const { results } = await inatjs.search( params );
|
||||
const records = results.map( result => {
|
||||
if ( sources === "taxa" ) {
|
||||
return result.taxon;
|
||||
}
|
||||
if ( sources === "places" ) {
|
||||
return result.place;
|
||||
}
|
||||
if ( sources === "users" ) {
|
||||
return result.user;
|
||||
}
|
||||
if ( sources === "projects" ) {
|
||||
return result.project;
|
||||
}
|
||||
return null;
|
||||
} );
|
||||
if ( !isCurrent ) { return; }
|
||||
setSearchResults( records );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( `Couldn't fetch search results with sources ${sources}:`, e.message );
|
||||
}
|
||||
};
|
||||
|
||||
// don't bother to fetch search results if there isn't a query
|
||||
if ( q === "" ) { return ( ) => {}; }
|
||||
fetchSearchResults( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [q, sources, fields] );
|
||||
|
||||
return searchResults;
|
||||
};
|
||||
|
||||
export default useRemoteSearchResults;
|
||||
@@ -1,44 +0,0 @@
|
||||
// @flow strict-local
|
||||
|
||||
import { Dimensions, StyleSheet } from "react-native";
|
||||
import type { TextStyleProp, ViewStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import colors from "styles/colors";
|
||||
|
||||
const { width } = Dimensions.get( "screen" );
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
imageBackground: {
|
||||
width: 75,
|
||||
height: 75,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.black,
|
||||
marginHorizontal: 20
|
||||
},
|
||||
obsDetailsColumn: {
|
||||
width: width / 3
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
marginVertical: 10
|
||||
},
|
||||
searchBar: {
|
||||
marginHorizontal: 10
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
text: { },
|
||||
greenText: {
|
||||
color: colors.inatGreen
|
||||
},
|
||||
explainerText: {
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 20
|
||||
}
|
||||
} );
|
||||
|
||||
export {
|
||||
textStyles,
|
||||
viewStyles
|
||||
};
|
||||
@@ -33,8 +33,7 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
flexDirection: "row"
|
||||
},
|
||||
column: {
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-evenly"
|
||||
paddingBottom: 200
|
||||
},
|
||||
profileImage: {
|
||||
height: 130,
|
||||
@@ -45,7 +44,7 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
width: 60
|
||||
},
|
||||
textInput: {
|
||||
backgroundColor: "#000000",
|
||||
backgroundColor: colors.white,
|
||||
borderWidth: 1,
|
||||
flexGrow: 1
|
||||
},
|
||||
|
||||
@@ -10,19 +10,9 @@ import AccessibilityEngine from "react-native-accessibility-engine";
|
||||
|
||||
import factory, { makeResponse } from "../factory";
|
||||
|
||||
const testUser = factory( "RemoteUser" );
|
||||
const mockExpected = testUser;
|
||||
|
||||
// Mock inaturalistjs so we can make some fake responses
|
||||
jest.mock( "inaturalistjs" );
|
||||
|
||||
jest.mock( "../../src/components/UserProfile/hooks/useRemoteUser", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
user: mockExpected
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
||||
import AddID from "components/ObsEdit/AddID";
|
||||
import inatjs from "inaturalistjs";
|
||||
@@ -21,22 +25,45 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
const testTaxaList = [
|
||||
{ taxon: factory( "RemoteTaxon" ) },
|
||||
{ taxon: factory( "RemoteTaxon" ) },
|
||||
{ taxon: factory( "RemoteTaxon" ) }
|
||||
const mockTaxaList = [
|
||||
factory( "RemoteTaxon" ),
|
||||
factory( "RemoteTaxon" ),
|
||||
factory( "RemoteTaxon" )
|
||||
];
|
||||
|
||||
const mockExpected = testTaxaList;
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
data: mockTaxaList
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock( "react-native-vector-icons/MaterialIcons", ( ) => {
|
||||
const InnerReact = require( "react" );
|
||||
class MaterialIcons extends InnerReact.Component {
|
||||
static getImageSourceSync( _thing, _number, _color ) {
|
||||
return { uri: "foo" };
|
||||
}
|
||||
|
||||
render( ) {
|
||||
return InnerReact.createElement( "MaterialIcons", this.props, this.props.children );
|
||||
}
|
||||
}
|
||||
return MaterialIcons;
|
||||
} );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderAddID = route => render(
|
||||
<NavigationContainer>
|
||||
<AddID route={route} />
|
||||
</NavigationContainer>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<AddID route={route} />
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
test( "renders taxon search results", async ( ) => {
|
||||
inatjs.search.mockResolvedValue( makeResponse( mockExpected ) );
|
||||
inatjs.search.mockResolvedValue( makeResponse( mockTaxaList ) );
|
||||
const route = { params: { } };
|
||||
const { getByTestId } = renderAddID( route );
|
||||
|
||||
@@ -45,7 +72,7 @@ test( "renders taxon search results", async ( ) => {
|
||||
fireEvent.changeText( input, "Some taxon" );
|
||||
} );
|
||||
|
||||
const { taxon } = testTaxaList[0];
|
||||
const taxon = mockTaxaList[0];
|
||||
|
||||
expect( getByTestId( `Search.taxa.${taxon.id}` ) ).toBeTruthy( );
|
||||
expect(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { fireEvent, render } from "@testing-library/react-native";
|
||||
import Explore from "components/Explore/Explore";
|
||||
import { ExploreContext } from "providers/contexts";
|
||||
@@ -12,6 +16,13 @@ const mockLatLng = {
|
||||
longitude: -122.42
|
||||
};
|
||||
|
||||
const mockUser = factory( "LocalUser" );
|
||||
|
||||
jest.mock( "sharedHooks/useCurrentUser", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => mockUser
|
||||
} ) );
|
||||
|
||||
jest.mock( "../../../../src/sharedHooks/useLoggedIn", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => true
|
||||
@@ -19,22 +30,21 @@ jest.mock( "../../../../src/sharedHooks/useLoggedIn", ( ) => ( {
|
||||
|
||||
// Mock the hooks we use on Map since we're not trying to test them here
|
||||
jest.mock( "../../../../src/sharedHooks/useUserLocation", ( ) => ( {
|
||||
default: ( ) => mockLatLng,
|
||||
__esModule: true
|
||||
__esModule: true,
|
||||
default: ( ) => mockLatLng
|
||||
} ) );
|
||||
|
||||
jest.mock( "../../../../src/sharedHooks/useLoggedIn", ( ) => ( {
|
||||
default: ( ) => false,
|
||||
__esModule: true
|
||||
// Some of the search inputs seem to query the API for some defaults, so this
|
||||
// tries to make sure they get nothing. It does so for all uses of
|
||||
// useAuthenticatedQuery, so watch this for unexpected behavior (but a unit
|
||||
// test really should not be making any network requests)
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( { data: null } )
|
||||
} ) );
|
||||
|
||||
jest.mock( "../../../../src/providers/ExploreProvider" );
|
||||
|
||||
jest.mock( "../../../../src/sharedHooks/useLoggedIn", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => true
|
||||
} ) );
|
||||
|
||||
// Mock ExploreProvider so it provides a specific array of observations
|
||||
// without any current observation or ability to update or fetch
|
||||
// observations
|
||||
@@ -66,12 +76,16 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderExplore = ( ) => render(
|
||||
<NavigationContainer>
|
||||
<ExploreProvider>
|
||||
<Explore />
|
||||
</ExploreProvider>
|
||||
</NavigationContainer>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<ExploreProvider>
|
||||
<Explore />
|
||||
</ExploreProvider>
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
// the next three tests are duplicates from ObsList.test.js, with Explore data
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { fireEvent, render } from "@testing-library/react-native";
|
||||
import ObsDetails from "components/ObsDetails/ObsDetails";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
@@ -9,19 +13,14 @@ import factory from "../../../factory";
|
||||
|
||||
const mockedNavigate = jest.fn( );
|
||||
const mockObservation = factory( "LocalObservation" );
|
||||
const mockUser = factory( "LocalUser" );
|
||||
|
||||
jest.mock( "../../../../src/providers/ObsEditProvider" );
|
||||
|
||||
jest.mock(
|
||||
"../../../../src/components/ObsDetails/hooks/useRemoteObservation",
|
||||
( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( _observation, _refetch ) => ( {
|
||||
remoteObservation: mockObservation,
|
||||
currentUserFaved: false
|
||||
} )
|
||||
} )
|
||||
);
|
||||
jest.mock( "sharedHooks/useCurrentUser", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => mockUser
|
||||
} ) );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
@@ -39,6 +38,13 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
data: mockObservation
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock( "../../../../src/components/LoginSignUp/AuthenticationService", ( ) => ( {
|
||||
getUserId: ( ) => mockObservation.user.id
|
||||
} ) );
|
||||
@@ -53,12 +59,16 @@ const mockObsEditProviderWithObs = ( ) => ObsEditProvider.mockImplementation( (
|
||||
</ObsEditContext.Provider>
|
||||
) );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderObsDetails = ( ) => render(
|
||||
<NavigationContainer>
|
||||
<ObsEditProvider>
|
||||
<ObsDetails />
|
||||
</ObsEditProvider>
|
||||
</NavigationContainer>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<ObsEditProvider>
|
||||
<ObsDetails />
|
||||
</ObsEditProvider>
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
test( "renders obs details from remote call", ( ) => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react-native";
|
||||
import ProjectDetails from "components/Projects/ProjectDetails";
|
||||
import React from "react";
|
||||
@@ -6,16 +10,12 @@ import React from "react";
|
||||
import factory from "../../../factory";
|
||||
|
||||
const mockProject = factory( "RemoteProject" );
|
||||
const mockObservation = factory( "RemoteObservation" );
|
||||
|
||||
jest.mock( "../../../../src/components/Projects/hooks/useProjectDetails", ( ) => ( {
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => mockProject
|
||||
} ) );
|
||||
|
||||
jest.mock( "../../../../src/components/Projects/hooks/useProjectObservations", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => [mockObservation]
|
||||
default: ( ) => ( {
|
||||
data: mockProject
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
@@ -30,10 +30,14 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderProjectDetails = ( ) => render(
|
||||
<NavigationContainer>
|
||||
<ProjectDetails />
|
||||
</NavigationContainer>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<ProjectDetails />
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
test( "displays project details", ( ) => {
|
||||
@@ -48,11 +52,3 @@ test( "displays project details", ( ) => {
|
||||
getByTestId( "ProjectDetails.projectIcon" ).props.source
|
||||
).toStrictEqual( { uri: mockProject.icon } );
|
||||
} );
|
||||
|
||||
test( "displays project observations", ( ) => {
|
||||
const { getByTestId, getByText } = renderProjectDetails( );
|
||||
|
||||
expect( getByText( mockObservation.taxon.preferred_common_name ) ).toBeTruthy( );
|
||||
expect( getByTestId( "ObsList.photo" ).props.source )
|
||||
.toStrictEqual( { uri: mockObservation.observation_photos[0].photo.url } );
|
||||
} );
|
||||
|
||||
50
tests/unit/components/Projects/ProjectObservations.test.js
Normal file
50
tests/unit/components/Projects/ProjectObservations.test.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react-native";
|
||||
import ProjectDetails from "components/Projects/ProjectDetails";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
|
||||
const mockProject = factory( "RemoteProject" );
|
||||
const mockObservation = factory( "RemoteObservation" );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
...actualNav,
|
||||
useRoute: ( ) => ( {
|
||||
params: {
|
||||
id: mockProject.id
|
||||
}
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderProjectDetails = ( ) => render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<ProjectDetails />
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
data: [mockObservation]
|
||||
} )
|
||||
} ) );
|
||||
|
||||
test( "displays project observations", ( ) => {
|
||||
const { getByTestId, getByText } = renderProjectDetails( );
|
||||
|
||||
expect( getByText( mockObservation.taxon.preferred_common_name ) ).toBeTruthy( );
|
||||
expect( getByTestId( "ObsList.photo" ).props.source )
|
||||
.toStrictEqual( { uri: mockObservation.observation_photos[0].photo.url } );
|
||||
} );
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { fireEvent, render } from "@testing-library/react-native";
|
||||
import Projects from "components/Projects/Projects";
|
||||
import React from "react";
|
||||
@@ -13,6 +17,13 @@ const mockLatLng = {
|
||||
longitude: -122.42
|
||||
};
|
||||
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
data: [mockProject]
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock( "../../../../src/sharedHooks/useLoggedIn", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => true
|
||||
@@ -24,11 +35,6 @@ jest.mock( "../../../../src/sharedHooks/useUserLocation", ( ) => ( {
|
||||
__esModule: true
|
||||
} ) );
|
||||
|
||||
jest.mock( "../../../../src/components/Projects/hooks/useProjects", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => [mockProject]
|
||||
} ) );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
@@ -39,10 +45,14 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderProjects = () => render(
|
||||
<NavigationContainer>
|
||||
<Projects />
|
||||
</NavigationContainer>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<Projects />
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
test( "displays project search results", ( ) => {
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { fireEvent, render } from "@testing-library/react-native";
|
||||
import Search from "components/Search/Search";
|
||||
import React from "react";
|
||||
|
||||
import factory from "../../../factory";
|
||||
|
||||
const testTaxaList = [
|
||||
factory( "RemoteTaxon" ),
|
||||
factory( "RemoteTaxon" ),
|
||||
factory( "RemoteTaxon" )
|
||||
];
|
||||
|
||||
const mockExpected = testTaxaList;
|
||||
jest.mock( "../../../../src/sharedHooks/useRemoteSearchResults", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => mockExpected
|
||||
} ) );
|
||||
|
||||
const mockedNavigate = jest.fn( );
|
||||
|
||||
const mockTaxon = factory( "RemoteTaxon" );
|
||||
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
data: [mockTaxon]
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
@@ -29,21 +30,23 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderSearch = ( ) => render(
|
||||
<NavigationContainer>
|
||||
<Search />
|
||||
</NavigationContainer>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<Search />
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
test( "renders taxon search results from API call", ( ) => {
|
||||
const { getByTestId, getByText } = renderSearch( );
|
||||
|
||||
const taxon = testTaxaList[0];
|
||||
|
||||
const commonName = taxon.preferred_common_name;
|
||||
const commonName = mockTaxon.preferred_common_name;
|
||||
expect( getByTestId( "Search.taxa" ) ).toBeTruthy( );
|
||||
expect( getByTestId( `Search.${taxon.id}.photo` ).props.source )
|
||||
.toStrictEqual( { uri: taxon.default_photo.square_url } );
|
||||
expect( getByTestId( `Search.${mockTaxon.id}.photo` ).props.source )
|
||||
.toStrictEqual( { uri: mockTaxon.default_photo.square_url } );
|
||||
// using RegExp to be able to search within a string
|
||||
expect( getByText( new RegExp( commonName ) ) ).toBeTruthy( );
|
||||
} );
|
||||
@@ -55,8 +58,6 @@ test.todo( "should not have accessibility errors" );
|
||||
test( "navigates to TaxonDetails on button press", ( ) => {
|
||||
const { getByTestId } = renderSearch( );
|
||||
|
||||
const taxon = testTaxaList[0];
|
||||
|
||||
fireEvent.press( getByTestId( `Search.taxa.${taxon.id}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "TaxonDetails", { id: taxon.id } );
|
||||
fireEvent.press( getByTestId( `Search.taxa.${mockTaxon.id}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "TaxonDetails", { id: mockTaxon.id } );
|
||||
} );
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { fireEvent, render } from "@testing-library/react-native";
|
||||
import Search from "components/Search/Search";
|
||||
import React from "react";
|
||||
@@ -10,6 +14,15 @@ import factory from "../../../factory";
|
||||
|
||||
const mockedNavigate = jest.fn( );
|
||||
|
||||
const mockUser = factory( "RemoteUser" );
|
||||
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
data: [mockUser]
|
||||
} )
|
||||
} ) );
|
||||
|
||||
jest.mock( "@react-navigation/native", ( ) => {
|
||||
const actualNav = jest.requireActual( "@react-navigation/native" );
|
||||
return {
|
||||
@@ -20,43 +33,35 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
};
|
||||
} );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderSearch = ( ) => render(
|
||||
<NavigationContainer>
|
||||
<Search />
|
||||
</NavigationContainer>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<Search />
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const testUserList = [
|
||||
factory( "RemoteUser" )
|
||||
];
|
||||
|
||||
const mockExpectedUsers = testUserList;
|
||||
jest.mock( "../../../../src/sharedHooks/useRemoteSearchResults", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => mockExpectedUsers
|
||||
} ) );
|
||||
const { login } = mockUser;
|
||||
|
||||
test( "displays user search results on button press", ( ) => {
|
||||
const { getByTestId, getByText } = renderSearch( );
|
||||
|
||||
const user = testUserList[0];
|
||||
const { login } = user;
|
||||
const button = getByTestId( "Search.users" );
|
||||
|
||||
fireEvent.press( button );
|
||||
expect( getByTestId( `Search.user.${login}` ) ).toBeTruthy( );
|
||||
expect( getByTestId( `Search.${login}.photo` ).props.source ).toStrictEqual( { uri: user.icon } );
|
||||
expect( getByTestId( `Search.${login}.photo` ).props.source ).toStrictEqual( {
|
||||
uri: mockUser.icon
|
||||
} );
|
||||
expect( getByText( new RegExp( login ) ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
test( "navigates to user profile on button press", ( ) => {
|
||||
const { getByTestId } = renderSearch( );
|
||||
|
||||
const user = testUserList[0];
|
||||
const { login } = user;
|
||||
const button = getByTestId( "Search.users" );
|
||||
|
||||
fireEvent.press( button );
|
||||
fireEvent.press( getByTestId( `Search.user.${login}` ) );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "UserProfile", { userId: user.id } );
|
||||
expect( mockedNavigate ).toHaveBeenCalledWith( "UserProfile", { userId: mockUser.id } );
|
||||
} );
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider
|
||||
} from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react-native";
|
||||
import UserProfile from "components/UserProfile/UserProfile";
|
||||
import React from "react";
|
||||
@@ -6,12 +10,12 @@ import React from "react";
|
||||
import factory from "../../../factory";
|
||||
|
||||
const testUser = factory( "RemoteUser" );
|
||||
const mockExpected = testUser;
|
||||
const mockUser = testUser;
|
||||
|
||||
jest.mock( "../../../../src/components/UserProfile/hooks/useRemoteUser", ( ) => ( {
|
||||
jest.mock( "sharedHooks/useAuthenticatedQuery", ( ) => ( {
|
||||
__esModule: true,
|
||||
default: ( ) => ( {
|
||||
user: mockExpected
|
||||
data: [mockUser]
|
||||
} )
|
||||
} ) );
|
||||
|
||||
@@ -21,16 +25,20 @@ jest.mock( "@react-navigation/native", ( ) => {
|
||||
...actualNav,
|
||||
useRoute: ( ) => ( {
|
||||
params: {
|
||||
userId: mockExpected.id
|
||||
userId: mockUser.id
|
||||
}
|
||||
} )
|
||||
};
|
||||
} );
|
||||
|
||||
const queryClient = new QueryClient( );
|
||||
|
||||
const renderUserProfile = ( ) => render(
|
||||
<NavigationContainer>
|
||||
<UserProfile />
|
||||
</NavigationContainer>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NavigationContainer>
|
||||
<UserProfile />
|
||||
</NavigationContainer>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
test( "renders user profile from API call", ( ) => {
|
||||
|
||||
Reference in New Issue
Block a user