mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Bug fixes from beta 0.0.1 (#75)
* Add exception handler library; fix computer vision results * Create loggedIn hook to check whether user is logged in before cv suggestions or upload * Create user profile card on MyObs * Update packages * Add text for camera permissions denied * Remove log * Upgrade react native on iOS * Add vendor file for ruby gems to gitignore * Remove vendor file from github * Update react native for android * Add plural example to fluent; create TranslatedText component to render translations * Add translations and update camera roll screens * Small changes to uploader flow; add date/time picker * Separate explore into landing screen and view screen * Show total number of observations, explore * Clean up styling and add details for grid view; banner for total observations in explore * Add checkboxes for status, quality grade, media filters * Add a lot of explore filters * Show months in Explore filters * Create About screen; sync package.json version using react native version * Get explore filters in mostly working condition * Observations download after login; clear login screen after nav; closes #62 and #60 * Allow separating photos if at least 1 combined photo obs is selected; closes #68 * Fix auth tests; add user id * Pop text input above keyboard to address #66 * Lint cleanup * Create bottom modal for user tapping back button on ObsEdit * Check permissions on android only, camera * Keep trying to get android camera working * Change version number to 0.1.0
This commit is contained in:
committed by
GitHub
parent
5a9aeb95ec
commit
1ebf4b951d
2
.bundle/config
Normal file
2
.bundle/config
Normal file
@@ -0,0 +1,2 @@
|
||||
BUNDLE_PATH: "vendor/bundle"
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: 1
|
||||
@@ -60,4 +60,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.149.0
|
||||
^0.162.0
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,3 +0,0 @@
|
||||
# Windows files should use crlf line endings
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
*.bat text eol=crlf
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -58,6 +58,9 @@ buck-out/
|
||||
# CocoaPods
|
||||
/ios/Pods/
|
||||
|
||||
# Ruby gems
|
||||
vendor/
|
||||
|
||||
# Realm
|
||||
*.realm*
|
||||
|
||||
|
||||
1
.ruby-version
Normal file
1
.ruby-version
Normal file
@@ -0,0 +1 @@
|
||||
2.7.4
|
||||
4
Gemfile
Normal file
4
Gemfile
Normal file
@@ -0,0 +1,4 @@
|
||||
source 'https://rubygems.org'
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby '2.7.4'
|
||||
gem 'cocoapods', '~> 1.11', '>= 1.11.2'
|
||||
100
Gemfile.lock
Normal file
100
Gemfile.lock
Normal file
@@ -0,0 +1,100 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
activesupport (6.1.5)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
atomos (0.1.3)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.11.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.11.3)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.4.0, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.4.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 1.0, < 3.0)
|
||||
xcodeproj (>= 1.21.0, < 2.0)
|
||||
cocoapods-core (1.11.3)
|
||||
activesupport (>= 5.0, < 7)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
netrc (~> 0.11)
|
||||
public_suffix (~> 4.0)
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.5)
|
||||
cocoapods-downloader (1.5.1)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.1)
|
||||
cocoapods-trunk (1.6.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.1.9)
|
||||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
ffi (1.15.5)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.6.1)
|
||||
minitest (5.15.0)
|
||||
molinillo (0.8.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
public_suffix (4.0.6)
|
||||
rexml (3.2.5)
|
||||
ruby-macho (2.5.1)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
xcodeproj (1.21.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods (~> 1.11, >= 1.11.2)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.4p191
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.27
|
||||
@@ -129,7 +129,7 @@ def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
/**
|
||||
* Whether to enable the Hermes VM.
|
||||
*
|
||||
* This should be set on project.ext.react and mirrored here. If it is not set
|
||||
* This should be set on project.ext.react and that value will be read here. If it is not set
|
||||
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
|
||||
* and the benefits of using Hermes will therefore be sharply reduced.
|
||||
*/
|
||||
@@ -144,8 +144,8 @@ android {
|
||||
applicationId "com.inaturalistreactnative"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 5
|
||||
versionName "0.1.0"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
29
android/app/src/main/res/drawable/rn_edit_text_material.xml
Normal file
29
android/app/src/main/res/drawable/rn_edit_text_material.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
</inset>
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:textColor">#000000</item>
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -23,8 +23,6 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url("$rootDir/../node_modules/react-native/android")
|
||||
@@ -33,6 +31,13 @@ allprojects {
|
||||
// Android JSC is installed from npm
|
||||
url("$rootDir/../node_modules/jsc-android/dist")
|
||||
}
|
||||
mavenCentral {
|
||||
// We don't want to fetch react-native from Maven Central as there are
|
||||
// older versions over there.
|
||||
content {
|
||||
excludeGroup "com.facebook.react"
|
||||
}
|
||||
}
|
||||
|
||||
google()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
rootProject.name = 'iNaturalistReactNative'
|
||||
include ':react-native-exception-handler'
|
||||
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android')
|
||||
include ':react-native-localize'
|
||||
project(':react-native-localize').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-localize/android')
|
||||
include ':react-native-pure-jwt'
|
||||
|
||||
31
index.js
31
index.js
@@ -2,12 +2,41 @@
|
||||
|
||||
import "react-native-gesture-handler";
|
||||
|
||||
import { AppRegistry } from "react-native";
|
||||
import { AppRegistry, Alert } from "react-native";
|
||||
import inatjs from "inaturalistjs";
|
||||
import App from "./src/navigation/rootNavigation";
|
||||
import {name as appName} from "./app.json";
|
||||
import "./src/i18n";
|
||||
import { startNetworkLogging } from "react-native-network-logger";
|
||||
import { setJSExceptionHandler } from "react-native-exception-handler";
|
||||
|
||||
// https://github.com/a7ul/react-native-exception-handler-example/blob/7c8f32d53856db1cc10f968c58034b285926951b/App.js
|
||||
const errorHandler = ( e, isFatal ) => {
|
||||
if ( isFatal ) {
|
||||
Alert.alert(
|
||||
"Unexpected error occurred",
|
||||
`
|
||||
Error: ${( isFatal ) ? "Fatal:" : ""} ${e.name} ${e.message}! Please close the app and start again!
|
||||
`,
|
||||
[{
|
||||
text: "Close"
|
||||
}]
|
||||
);
|
||||
}
|
||||
console.log( e ); // So that we can see it in the ADB logs in case of Android if needed
|
||||
};
|
||||
|
||||
setJSExceptionHandler( errorHandler, true );
|
||||
|
||||
//For most use cases:
|
||||
// setNativeExceptionHandler( ( exceptionString ) => {
|
||||
// console.log( exceptionString, "exception string native" );
|
||||
// // alert( exceptionString );
|
||||
// Alert.alert(
|
||||
// "",
|
||||
// exceptionString
|
||||
// );
|
||||
// } );
|
||||
|
||||
startNetworkLogging();
|
||||
|
||||
|
||||
@@ -18,12 +18,6 @@ target 'iNaturalistReactNative' do
|
||||
|
||||
pod 'react-native-config', :path => '../node_modules/react-native-config'
|
||||
|
||||
|
||||
target 'iNaturalistReactNativeTests' do
|
||||
inherit! :complete
|
||||
# Pods for testing
|
||||
end
|
||||
|
||||
# Enables Flipper.
|
||||
#
|
||||
# Note that if you have use_frameworks! enabled, Flipper will not work and
|
||||
|
||||
486
ios/Podfile.lock
486
ios/Podfile.lock
@@ -1,17 +1,17 @@
|
||||
PODS:
|
||||
- boost (1.76.0)
|
||||
- DoubleConversion (1.1.6)
|
||||
- FBLazyVector (0.66.4)
|
||||
- FBReactNativeSpec (0.66.4):
|
||||
- FBLazyVector (0.67.4)
|
||||
- FBReactNativeSpec (0.67.4):
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- RCTRequired (= 0.66.4)
|
||||
- RCTTypeSafety (= 0.66.4)
|
||||
- React-Core (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- RCTRequired (= 0.67.4)
|
||||
- RCTTypeSafety (= 0.67.4)
|
||||
- React-Core (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- fmt (6.2.1)
|
||||
- glog (0.3.5)
|
||||
- Permission-LocationWhenInUse (3.1.0):
|
||||
- Permission-LocationWhenInUse (3.3.1):
|
||||
- RNPermissions
|
||||
- RCT-Folly (2021.06.28.00-v2):
|
||||
- boost
|
||||
@@ -24,192 +24,192 @@ PODS:
|
||||
- DoubleConversion
|
||||
- fmt (~> 6.2.1)
|
||||
- glog
|
||||
- RCTRequired (0.66.4)
|
||||
- RCTTypeSafety (0.66.4):
|
||||
- FBLazyVector (= 0.66.4)
|
||||
- RCTRequired (0.67.4)
|
||||
- RCTTypeSafety (0.67.4):
|
||||
- FBLazyVector (= 0.67.4)
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- RCTRequired (= 0.66.4)
|
||||
- React-Core (= 0.66.4)
|
||||
- React (0.66.4):
|
||||
- React-Core (= 0.66.4)
|
||||
- React-Core/DevSupport (= 0.66.4)
|
||||
- React-Core/RCTWebSocket (= 0.66.4)
|
||||
- React-RCTActionSheet (= 0.66.4)
|
||||
- React-RCTAnimation (= 0.66.4)
|
||||
- React-RCTBlob (= 0.66.4)
|
||||
- React-RCTImage (= 0.66.4)
|
||||
- React-RCTLinking (= 0.66.4)
|
||||
- React-RCTNetwork (= 0.66.4)
|
||||
- React-RCTSettings (= 0.66.4)
|
||||
- React-RCTText (= 0.66.4)
|
||||
- React-RCTVibration (= 0.66.4)
|
||||
- React-callinvoker (0.66.4)
|
||||
- React-Core (0.66.4):
|
||||
- RCTRequired (= 0.67.4)
|
||||
- React-Core (= 0.67.4)
|
||||
- React (0.67.4):
|
||||
- React-Core (= 0.67.4)
|
||||
- React-Core/DevSupport (= 0.67.4)
|
||||
- React-Core/RCTWebSocket (= 0.67.4)
|
||||
- React-RCTActionSheet (= 0.67.4)
|
||||
- React-RCTAnimation (= 0.67.4)
|
||||
- React-RCTBlob (= 0.67.4)
|
||||
- React-RCTImage (= 0.67.4)
|
||||
- React-RCTLinking (= 0.67.4)
|
||||
- React-RCTNetwork (= 0.67.4)
|
||||
- React-RCTSettings (= 0.67.4)
|
||||
- React-RCTText (= 0.67.4)
|
||||
- React-RCTVibration (= 0.67.4)
|
||||
- React-callinvoker (0.67.4)
|
||||
- React-Core (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default (= 0.66.4)
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-Core/Default (= 0.67.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/CoreModulesHeaders (0.66.4):
|
||||
- React-Core/CoreModulesHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/Default (0.66.4):
|
||||
- React-Core/Default (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/DevSupport (0.66.4):
|
||||
- React-Core/DevSupport (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default (= 0.66.4)
|
||||
- React-Core/RCTWebSocket (= 0.66.4)
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-jsinspector (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-Core/Default (= 0.67.4)
|
||||
- React-Core/RCTWebSocket (= 0.67.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-jsinspector (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTActionSheetHeaders (0.66.4):
|
||||
- React-Core/RCTActionSheetHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTAnimationHeaders (0.66.4):
|
||||
- React-Core/RCTAnimationHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTBlobHeaders (0.66.4):
|
||||
- React-Core/RCTBlobHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTImageHeaders (0.66.4):
|
||||
- React-Core/RCTImageHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTLinkingHeaders (0.66.4):
|
||||
- React-Core/RCTLinkingHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTNetworkHeaders (0.66.4):
|
||||
- React-Core/RCTNetworkHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTSettingsHeaders (0.66.4):
|
||||
- React-Core/RCTSettingsHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTTextHeaders (0.66.4):
|
||||
- React-Core/RCTTextHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTVibrationHeaders (0.66.4):
|
||||
- React-Core/RCTVibrationHeaders (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-Core/RCTWebSocket (0.66.4):
|
||||
- React-Core/RCTWebSocket (0.67.4):
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/Default (= 0.66.4)
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsiexecutor (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-Core/Default (= 0.67.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsiexecutor (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- Yoga
|
||||
- React-CoreModules (0.66.4):
|
||||
- FBReactNativeSpec (= 0.66.4)
|
||||
- React-CoreModules (0.67.4):
|
||||
- FBReactNativeSpec (= 0.67.4)
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- RCTTypeSafety (= 0.66.4)
|
||||
- React-Core/CoreModulesHeaders (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-RCTImage (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- React-cxxreact (0.66.4):
|
||||
- RCTTypeSafety (= 0.67.4)
|
||||
- React-Core/CoreModulesHeaders (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-RCTImage (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- React-cxxreact (0.67.4):
|
||||
- boost (= 1.76.0)
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-callinvoker (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-jsinspector (= 0.66.4)
|
||||
- React-logger (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-runtimeexecutor (= 0.66.4)
|
||||
- React-jsi (0.66.4):
|
||||
- React-callinvoker (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-jsinspector (= 0.67.4)
|
||||
- React-logger (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- React-runtimeexecutor (= 0.67.4)
|
||||
- React-jsi (0.67.4):
|
||||
- boost (= 1.76.0)
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-jsi/Default (= 0.66.4)
|
||||
- React-jsi/Default (0.66.4):
|
||||
- React-jsi/Default (= 0.67.4)
|
||||
- React-jsi/Default (0.67.4):
|
||||
- boost (= 1.76.0)
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-jsiexecutor (0.66.4):
|
||||
- React-jsiexecutor (0.67.4):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-jsinspector (0.66.4)
|
||||
- React-logger (0.66.4):
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- React-jsinspector (0.67.4)
|
||||
- React-logger (0.67.4):
|
||||
- glog
|
||||
- react-native-cameraroll (4.1.2):
|
||||
- React-Core
|
||||
@@ -223,92 +223,102 @@ PODS:
|
||||
- React
|
||||
- react-native-image-resizer (1.4.5):
|
||||
- React-Core
|
||||
- react-native-maps (0.29.3):
|
||||
- react-native-maps (0.30.1):
|
||||
- React-Core
|
||||
- react-native-netinfo (7.1.7):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (3.3.2):
|
||||
- react-native-netinfo (8.2.0):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.2.2):
|
||||
- RCT-Folly
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React
|
||||
- ReactCommon/turbomodule/core
|
||||
- react-native-sensitive-info (6.0.0-alpha.9):
|
||||
- React-Core
|
||||
- React-perflogger (0.66.4)
|
||||
- React-RCTActionSheet (0.66.4):
|
||||
- React-Core/RCTActionSheetHeaders (= 0.66.4)
|
||||
- React-RCTAnimation (0.66.4):
|
||||
- FBReactNativeSpec (= 0.66.4)
|
||||
- React-perflogger (0.67.4)
|
||||
- React-RCTActionSheet (0.67.4):
|
||||
- React-Core/RCTActionSheetHeaders (= 0.67.4)
|
||||
- React-RCTAnimation (0.67.4):
|
||||
- FBReactNativeSpec (= 0.67.4)
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- RCTTypeSafety (= 0.66.4)
|
||||
- React-Core/RCTAnimationHeaders (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- React-RCTBlob (0.66.4):
|
||||
- FBReactNativeSpec (= 0.66.4)
|
||||
- RCTTypeSafety (= 0.67.4)
|
||||
- React-Core/RCTAnimationHeaders (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- React-RCTBlob (0.67.4):
|
||||
- FBReactNativeSpec (= 0.67.4)
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/RCTBlobHeaders (= 0.66.4)
|
||||
- React-Core/RCTWebSocket (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-RCTNetwork (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- React-RCTImage (0.66.4):
|
||||
- FBReactNativeSpec (= 0.66.4)
|
||||
- React-Core/RCTBlobHeaders (= 0.67.4)
|
||||
- React-Core/RCTWebSocket (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-RCTNetwork (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- React-RCTImage (0.67.4):
|
||||
- FBReactNativeSpec (= 0.67.4)
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- RCTTypeSafety (= 0.66.4)
|
||||
- React-Core/RCTImageHeaders (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-RCTNetwork (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- React-RCTLinking (0.66.4):
|
||||
- FBReactNativeSpec (= 0.66.4)
|
||||
- React-Core/RCTLinkingHeaders (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- React-RCTNetwork (0.66.4):
|
||||
- FBReactNativeSpec (= 0.66.4)
|
||||
- RCTTypeSafety (= 0.67.4)
|
||||
- React-Core/RCTImageHeaders (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-RCTNetwork (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- React-RCTLinking (0.67.4):
|
||||
- FBReactNativeSpec (= 0.67.4)
|
||||
- React-Core/RCTLinkingHeaders (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- React-RCTNetwork (0.67.4):
|
||||
- FBReactNativeSpec (= 0.67.4)
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- RCTTypeSafety (= 0.66.4)
|
||||
- React-Core/RCTNetworkHeaders (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- React-RCTSettings (0.66.4):
|
||||
- FBReactNativeSpec (= 0.66.4)
|
||||
- RCTTypeSafety (= 0.67.4)
|
||||
- React-Core/RCTNetworkHeaders (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- React-RCTSettings (0.67.4):
|
||||
- FBReactNativeSpec (= 0.67.4)
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- RCTTypeSafety (= 0.66.4)
|
||||
- React-Core/RCTSettingsHeaders (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- React-RCTText (0.66.4):
|
||||
- React-Core/RCTTextHeaders (= 0.66.4)
|
||||
- React-RCTVibration (0.66.4):
|
||||
- FBReactNativeSpec (= 0.66.4)
|
||||
- RCTTypeSafety (= 0.67.4)
|
||||
- React-Core/RCTSettingsHeaders (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- React-RCTText (0.67.4):
|
||||
- React-Core/RCTTextHeaders (= 0.67.4)
|
||||
- React-RCTVibration (0.67.4):
|
||||
- FBReactNativeSpec (= 0.67.4)
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-Core/RCTVibrationHeaders (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (= 0.66.4)
|
||||
- React-runtimeexecutor (0.66.4):
|
||||
- React-jsi (= 0.66.4)
|
||||
- ReactCommon/turbomodule/core (0.66.4):
|
||||
- React-Core/RCTVibrationHeaders (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (= 0.67.4)
|
||||
- React-runtimeexecutor (0.67.4):
|
||||
- React-jsi (= 0.67.4)
|
||||
- ReactCommon/turbomodule/core (0.67.4):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- RCT-Folly (= 2021.06.28.00-v2)
|
||||
- React-callinvoker (= 0.66.4)
|
||||
- React-Core (= 0.66.4)
|
||||
- React-cxxreact (= 0.66.4)
|
||||
- React-jsi (= 0.66.4)
|
||||
- React-logger (= 0.66.4)
|
||||
- React-perflogger (= 0.66.4)
|
||||
- React-callinvoker (= 0.67.4)
|
||||
- React-Core (= 0.67.4)
|
||||
- React-cxxreact (= 0.67.4)
|
||||
- React-jsi (= 0.67.4)
|
||||
- React-logger (= 0.67.4)
|
||||
- React-perflogger (= 0.67.4)
|
||||
- ReactNativeExceptionHandler (2.10.10):
|
||||
- React-Core
|
||||
- RealmJS (10.20.0-beta.1):
|
||||
- React
|
||||
- RNAudioRecorderPlayer (3.3.0):
|
||||
- RNAudioRecorderPlayer (3.3.4):
|
||||
- React-Core
|
||||
- RNCPicker (2.2.1):
|
||||
- RNCCheckbox (0.5.12):
|
||||
- React-Core
|
||||
- RNDeviceInfo (8.4.9):
|
||||
- RNCPicker (2.4.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (6.1.0):
|
||||
- React-Core
|
||||
- RNDeviceInfo (8.5.1):
|
||||
- React-Core
|
||||
- RNGestureHandler (1.10.3):
|
||||
- React-Core
|
||||
- RNLocalize (2.1.7):
|
||||
- RNLocalize (2.2.1):
|
||||
- React-Core
|
||||
- RNPermissions (3.1.0):
|
||||
- RNPermissions (3.3.1):
|
||||
- React-Core
|
||||
- RNReanimated (2.3.1):
|
||||
- DoubleConversion
|
||||
@@ -341,7 +351,7 @@ PODS:
|
||||
- RNScreens (3.8.0):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- VisionCamera (2.12.0):
|
||||
- VisionCamera (2.13.0):
|
||||
- React
|
||||
- React-callinvoker
|
||||
- React-Core
|
||||
@@ -389,9 +399,12 @@ DEPENDENCIES:
|
||||
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
|
||||
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||
- ReactNativeExceptionHandler (from `../node_modules/react-native-exception-handler`)
|
||||
- RealmJS (from `../node_modules/realm`)
|
||||
- RNAudioRecorderPlayer (from `../node_modules/react-native-audio-recorder-player`)
|
||||
- "RNCCheckbox (from `../node_modules/@react-native-community/checkbox`)"
|
||||
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNLocalize (from `../node_modules/react-native-localize`)
|
||||
@@ -484,12 +497,18 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
|
||||
ReactCommon:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
ReactNativeExceptionHandler:
|
||||
:path: "../node_modules/react-native-exception-handler"
|
||||
RealmJS:
|
||||
:path: "../node_modules/realm"
|
||||
RNAudioRecorderPlayer:
|
||||
:path: "../node_modules/react-native-audio-recorder-player"
|
||||
RNCCheckbox:
|
||||
:path: "../node_modules/@react-native-community/checkbox"
|
||||
RNCPicker:
|
||||
:path: "../node_modules/@react-native-picker/picker"
|
||||
RNDateTimePicker:
|
||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||
RNDeviceInfo:
|
||||
:path: "../node_modules/react-native-device-info"
|
||||
RNGestureHandler:
|
||||
@@ -510,56 +529,59 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
||||
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
|
||||
FBLazyVector: e5569e42a1c79ca00521846c223173a57aca1fe1
|
||||
FBReactNativeSpec: fe08c1cd7e2e205718d77ad14b34957cce949b58
|
||||
FBLazyVector: f7b0632c6437e312acf6349288d9aa4cb6d59030
|
||||
FBReactNativeSpec: 0f4e1f4cfeace095694436e7c7fcc5bf4b03a0ff
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
glog: 5337263514dd6f09803962437687240c5dc39aa4
|
||||
Permission-LocationWhenInUse: d98db702ec75e93a3ff94bc297d0b66ea04231e1
|
||||
RCT-Folly: a21c126816d8025b547704b777a2ba552f3d9fa9
|
||||
RCTRequired: 4bf86c70714490bca4bf2696148638284622644b
|
||||
RCTTypeSafety: c475a7059eb77935fa53d2c17db299893f057d5d
|
||||
React: f64af14e3f2c50f6f2c91a5fd250e4ff1b3c3459
|
||||
React-callinvoker: b74e4ae80287780dcdf0cab262bcb581eeef56e7
|
||||
React-Core: 3eb7432bad96ff1d25aebc1defbae013fee2fd0e
|
||||
React-CoreModules: ad9e1fd5650e16666c57a08328df86fd7e480cb9
|
||||
React-cxxreact: 02633ff398cf7e91a2c1e12590d323c4a4b8668a
|
||||
React-jsi: 805c41a927d6499fb811772acb971467d9204633
|
||||
React-jsiexecutor: 94ce921e1d8ce7023366873ec371f3441383b396
|
||||
React-jsinspector: d0374f7509d407d2264168b6d0fad0b54e300b85
|
||||
React-logger: 933f80c97c633ee8965d609876848148e3fef438
|
||||
glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85
|
||||
Permission-LocationWhenInUse: 006c85c8de0c05b5d8be8e8029e4f6b813270293
|
||||
RCT-Folly: 803a9cfd78114b2ec0f140cfa6fa2a6bafb2d685
|
||||
RCTRequired: 0aa6c1c27e1d65920df35ceea5341a5fe76bdb79
|
||||
RCTTypeSafety: d76a59d00632891e11ed7522dba3fd1a995e573a
|
||||
React: ab8c09da2e7704f4b3ebad4baa6cfdfcc852dcb5
|
||||
React-callinvoker: 216fb96b482da516b8aba4142b145938f6ea92f0
|
||||
React-Core: af99b93aff83599485e0e0879879aafa35ceae32
|
||||
React-CoreModules: 137a054ce8c547e81dc3502933b1bc0fd08df05d
|
||||
React-cxxreact: ec5ee6b08664f5b8ac71d8ad912f54d540c4f817
|
||||
React-jsi: 3e084c80fd364cee64668d5df46d40c39f7973e1
|
||||
React-jsiexecutor: cbdf37cebdc4f5d8b3d0bf5ccaa6147fd9de9f3d
|
||||
React-jsinspector: f4775ea9118cbe1f72b834f0f842baa7a99508d8
|
||||
React-logger: a1f028f6d8639a3f364ef80419e5e862e1115250
|
||||
react-native-cameraroll: 2957f2bce63ae896a848fbe0d5352c1bd4d20866
|
||||
react-native-config: 6502b1879f97ed5ac570a029961fc35ea606cd14
|
||||
react-native-geocoder: 757427682892bb256f3b3745858cc90eba148a8e
|
||||
react-native-geolocation-service: c0efb872258ed9240f1003a70fca9e9757e5c785
|
||||
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
|
||||
react-native-maps: 41d01d8e0afcebe32bec9eea3bd945adc1b18f7a
|
||||
react-native-netinfo: 27f287f2d191693f3b9d01a4273137fcf91c3b5d
|
||||
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
|
||||
react-native-maps: d752b0dd0e1951d815b1336332835aab6b4a836f
|
||||
react-native-netinfo: e922cb2e3eaf9ccdf16b8d4744a89657377aa4a1
|
||||
react-native-safe-area-context: da2d11bd7df9bf7779e9bdc85081c141cfa544f4
|
||||
react-native-sensitive-info: d44e909d065f9c0e15734245e5dd6a24b82e3dcd
|
||||
React-perflogger: 93075d8931c32cd1fce8a98c15d2d5ccc4d891bd
|
||||
React-RCTActionSheet: 7d3041e6761b4f3044a37079ddcb156575fb6d89
|
||||
React-RCTAnimation: 743e88b55ac62511ae5c2e22803d4f503f2a3a13
|
||||
React-RCTBlob: bee3a2f98fa7fc25c957c8643494244f74bea0a0
|
||||
React-RCTImage: 19fc9e29b06cc38611c553494f8d3040bf78c24e
|
||||
React-RCTLinking: dc799503979c8c711126d66328e7ce8f25c2848f
|
||||
React-RCTNetwork: 417e4e34cf3c19eaa5fd4e9eb20180d662a799ce
|
||||
React-RCTSettings: 4df89417265af26501a7e0e9192a34d3d9848dff
|
||||
React-RCTText: f8a21c3499ab322326290fa9b701ae29aa093aa5
|
||||
React-RCTVibration: e3ffca672dd3772536cb844274094b0e2c31b187
|
||||
React-runtimeexecutor: dec32ee6f2e2a26e13e58152271535fadff5455a
|
||||
ReactCommon: 57b69f6383eafcbd7da625bfa6003810332313c4
|
||||
React-perflogger: 0afaf2f01a47fd0fc368a93bfbb5bd3b26db6e7f
|
||||
React-RCTActionSheet: 59f35c4029e0b532fc42114241a06e170b7431a2
|
||||
React-RCTAnimation: aae4f4bed122e78bdab72f7118d291d70a932ce2
|
||||
React-RCTBlob: f6fb23394b4f28cd86fa7e9f5f6ae45c23669fda
|
||||
React-RCTImage: 638815cf96124386dd296067246d91441932ae3f
|
||||
React-RCTLinking: 254dd06283dd6fdb784285f95e7cec8053c3270f
|
||||
React-RCTNetwork: 8a4c2d4f357268e520b060572d02bc69a9b991fb
|
||||
React-RCTSettings: 35d44cbb9972ab933bd0a59ea3e6646dcb030ba3
|
||||
React-RCTText: cc5315df8458cfa7b537e621271ef43273955a97
|
||||
React-RCTVibration: 3b52a7dced19cdb025b4f88ab26ceb2d85f30ba2
|
||||
React-runtimeexecutor: a9d3c82ddf7ffdad9fbe6a81c6d6f8c06385464d
|
||||
ReactCommon: 07d0c460b9ba9af3eaf1b8f5abe7daaad28c9c4e
|
||||
ReactNativeExceptionHandler: b11ff67c78802b2f62eed0e10e75cb1ef7947c60
|
||||
RealmJS: 74cf2dec0a20e7ca75655b5190eb60c062d106ec
|
||||
RNAudioRecorderPlayer: 413c69a85412df8476e1dfcc17b3429f62e02ecd
|
||||
RNCPicker: cb57c823d5ce8d2d0b5dfb45ad97b737260dc59e
|
||||
RNDeviceInfo: 4944cf8787b9c5bffaf301fda68cc1a2ec003341
|
||||
RNAudioRecorderPlayer: 4efbe1839fd21c5caf8de132a8b3b51b422aa997
|
||||
RNCCheckbox: ed1b4ca295475b41e7251ebae046360a703b6eb5
|
||||
RNCPicker: 6d5d64e7b90c240c779ee0938ec433c11e2dd758
|
||||
RNDateTimePicker: 064f3a609fbebc6896f7e5a2f48dcee5d9a6fd51
|
||||
RNDeviceInfo: 8d4177859b062334835962799460528869a487fb
|
||||
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
|
||||
RNLocalize: f567ea0e35116a641cdffe6683b0d212d568f32a
|
||||
RNPermissions: 4b54095940aea8c03fa3e6c92d4ac3647b31ed4e
|
||||
RNReanimated: da3860204e5660c0dd66739936732197d359d753
|
||||
RNLocalize: cbcb55d0e19c78086ea4eea20e03fe8000bbbced
|
||||
RNPermissions: 34d678157c800b25b22a488e4d8babb57456e796
|
||||
RNReanimated: 1326679461fa5d2399d54c18ca1432ba3e816b9e
|
||||
RNScreens: 6e1ea5787989f92b0671049b808aef64fa1ef98c
|
||||
VisionCamera: e3f4eea37f32f9a2ab40ce560940174a5258abaf
|
||||
Yoga: e7dc4e71caba6472ff48ad7d234389b91dadc280
|
||||
VisionCamera: f6ebc7a6be166f3cd5744972b9ae394ca15a5145
|
||||
Yoga: d6b6a80659aa3e91aaba01d0012e7edcbedcbecd
|
||||
|
||||
PODFILE CHECKSUM: b4cc6dd668e4126499955bf845bca9b702156b71
|
||||
PODFILE CHECKSUM: 55a351af61798294e5bd5ec55bf493996157f1aa
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
6414F5CE81CE19F4E4D5FEB9 /* libPods-iNaturalistReactNative-iNaturalistReactNativeTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4861934FC5BA104D45FB5B93 /* libPods-iNaturalistReactNative-iNaturalistReactNativeTests.a */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
C15FC29A57139D5E79AEE225 /* libPods-iNaturalistReactNative.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94E8E83754985D3480D4250B /* libPods-iNaturalistReactNative.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -36,12 +35,9 @@
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = iNaturalistReactNative/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = iNaturalistReactNative/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = iNaturalistReactNative/main.m; sourceTree = "<group>"; };
|
||||
4861934FC5BA104D45FB5B93 /* libPods-iNaturalistReactNative-iNaturalistReactNativeTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iNaturalistReactNative-iNaturalistReactNativeTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
613C68439CBDA5A564FC47C9 /* Pods-iNaturalistReactNative-iNaturalistReactNativeTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative-iNaturalistReactNativeTests.debug.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative-iNaturalistReactNativeTests/Pods-iNaturalistReactNative-iNaturalistReactNativeTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = iNaturalistReactNative/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
82C3EA8411717C12B2D638F0 /* Pods-iNaturalistReactNative.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative.debug.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative/Pods-iNaturalistReactNative.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
94E8E83754985D3480D4250B /* libPods-iNaturalistReactNative.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iNaturalistReactNative.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ADEAA14C1CEC2FA93BB66671 /* Pods-iNaturalistReactNative-iNaturalistReactNativeTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative-iNaturalistReactNativeTests.release.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative-iNaturalistReactNativeTests/Pods-iNaturalistReactNative-iNaturalistReactNativeTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
E6BB4561D9EBE3B509576120 /* Pods-iNaturalistReactNative.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative.release.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative/Pods-iNaturalistReactNative.release.xcconfig"; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -51,7 +47,6 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6414F5CE81CE19F4E4D5FEB9 /* libPods-iNaturalistReactNative-iNaturalistReactNativeTests.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -101,7 +96,6 @@
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
94E8E83754985D3480D4250B /* libPods-iNaturalistReactNative.a */,
|
||||
4861934FC5BA104D45FB5B93 /* libPods-iNaturalistReactNative-iNaturalistReactNativeTests.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -111,8 +105,6 @@
|
||||
children = (
|
||||
82C3EA8411717C12B2D638F0 /* Pods-iNaturalistReactNative.debug.xcconfig */,
|
||||
E6BB4561D9EBE3B509576120 /* Pods-iNaturalistReactNative.release.xcconfig */,
|
||||
613C68439CBDA5A564FC47C9 /* Pods-iNaturalistReactNative-iNaturalistReactNativeTests.debug.xcconfig */,
|
||||
ADEAA14C1CEC2FA93BB66671 /* Pods-iNaturalistReactNative-iNaturalistReactNativeTests.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -155,11 +147,9 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "iNaturalistReactNativeTests" */;
|
||||
buildPhases = (
|
||||
92A643C32A80886C9FAA1A77 /* [CP] Check Pods Manifest.lock */,
|
||||
00E356EA1AD99517003FC87E /* Sources */,
|
||||
00E356EB1AD99517003FC87E /* Frameworks */,
|
||||
00E356EC1AD99517003FC87E /* Resources */,
|
||||
D5330C557867060D11370BCC /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -284,45 +274,6 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
92A643C32A80886C9FAA1A77 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-iNaturalistReactNative-iNaturalistReactNativeTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
D5330C557867060D11370BCC /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-iNaturalistReactNative-iNaturalistReactNativeTests/Pods-iNaturalistReactNative-iNaturalistReactNativeTests-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-iNaturalistReactNative-iNaturalistReactNativeTests/Pods-iNaturalistReactNative-iNaturalistReactNativeTests-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iNaturalistReactNative-iNaturalistReactNativeTests/Pods-iNaturalistReactNative-iNaturalistReactNativeTests-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
D7FDE6CB7769C45385E67CAA /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -392,7 +343,6 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
00E356F61AD99517003FC87E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 613C68439CBDA5A564FC47C9 /* Pods-iNaturalistReactNative-iNaturalistReactNativeTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -419,7 +369,6 @@
|
||||
};
|
||||
00E356F71AD99517003FC87E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = ADEAA14C1CEC2FA93BB66671 /* Pods-iNaturalistReactNative-iNaturalistReactNativeTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>5</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
@@ -31,7 +31,7 @@
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<true />
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
@@ -56,6 +56,6 @@
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<false />
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>4</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
2335
package-lock.json
generated
2335
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "inaturalistreactnative",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
@@ -12,76 +12,80 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-community/cameraroll": "^4.1.2",
|
||||
"@react-native-community/netinfo": "^7.1.7",
|
||||
"@react-native-picker/picker": "^2.2.1",
|
||||
"@react-navigation/drawer": "^6.1.8",
|
||||
"@react-navigation/elements": "^1.2.1",
|
||||
"@react-navigation/native": "^6.0.6",
|
||||
"@react-navigation/native-stack": "^6.2.4",
|
||||
"@react-native-community/checkbox": "^0.5.12",
|
||||
"@react-native-community/datetimepicker": "^6.1.0",
|
||||
"@react-native-community/netinfo": "^8.2.0",
|
||||
"@react-native-picker/picker": "^2.4.0",
|
||||
"@react-navigation/drawer": "^6.3.1",
|
||||
"@react-navigation/elements": "^1.3.1",
|
||||
"@react-navigation/native": "^6.0.8",
|
||||
"@react-navigation/native-stack": "^6.5.2",
|
||||
"apisauce": "^2.1.2",
|
||||
"axios": "^0.25.0",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
|
||||
"date-fns": "^2.28.0",
|
||||
"i18next": "^21.6.6",
|
||||
"i18next": "^21.6.14",
|
||||
"i18next-fluent": "^2.0.0",
|
||||
"i18next-resources-to-backend": "^1.0.0",
|
||||
"inaturalistjs": "github:inaturalist/inaturalistjs",
|
||||
"radio-buttons-react-native": "^1.0.4",
|
||||
"react": "17.0.2",
|
||||
"react-i18next": "^11.15.3",
|
||||
"react-native": "0.66.4",
|
||||
"react-native-audio-recorder-player": "^3.3.0",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-native": "^0.67.4",
|
||||
"react-native-audio-recorder-player": "^3.3.4",
|
||||
"react-native-config": "^1.4.5",
|
||||
"react-native-device-info": "^8.4.8",
|
||||
"react-native-dropdown-picker": "^5.2.3",
|
||||
"react-native-device-info": "^8.5.1",
|
||||
"react-native-dropdown-picker": "^5.3.0",
|
||||
"react-native-exception-handler": "^2.10.10",
|
||||
"react-native-geocoder": "^0.5.0",
|
||||
"react-native-geolocation-service": "^5.3.0-beta.4",
|
||||
"react-native-gesture-handler": "^1.10.3",
|
||||
"react-native-image-resizer": "^1.4.5",
|
||||
"react-native-jwt-io": "^1.0.3",
|
||||
"react-native-localize": "^2.1.7",
|
||||
"react-native-maps": "^0.29.3",
|
||||
"react-native-modal": "^13.0.0",
|
||||
"react-native-modal-datetime-picker": "^13.0.0",
|
||||
"react-native-localize": "^2.2.1",
|
||||
"react-native-maps": "^0.30.1",
|
||||
"react-native-modal": "^13.0.1",
|
||||
"react-native-modal-datetime-picker": "^13.1.0",
|
||||
"react-native-network-logger": "^1.12.0",
|
||||
"react-native-permissions": "^3.1.0",
|
||||
"react-native-permissions": "^3.3.1",
|
||||
"react-native-picker-select": "^8.0.4",
|
||||
"react-native-reanimated": "2.3.1",
|
||||
"react-native-render-html": "^6.3.0",
|
||||
"react-native-safe-area-context": "^3.3.2",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "^4.2.2",
|
||||
"react-native-screens": "^3.8.0",
|
||||
"react-native-sensitive-info": "^6.0.0-alpha.9",
|
||||
"react-native-uuid": "^2.0.1",
|
||||
"react-native-vision-camera": "^2.12.0",
|
||||
"react-native-vision-camera": "^2.13.0",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-tinder-card": "^1.4.5",
|
||||
"realm": "10.20.0-beta.1"
|
||||
"realm": "^10.20.0-beta.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@react-native-community/eslint-config": "^2.0.0",
|
||||
"@react-native-community/eslint-config": "^3.0.1",
|
||||
"@testing-library/react-native": "^9.0.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-flowtype": "^6.0.1",
|
||||
"eslint-plugin-jest": "^24.4.0",
|
||||
"eslint-plugin-flowtype": "^7.0.0",
|
||||
"eslint-plugin-jest": "^26.1.3",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.25.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-react-native": "^3.11.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-react-native": "^4.0.0",
|
||||
"factoria": "^3.2.2",
|
||||
"faker": "^5.5.3",
|
||||
"flow-bin": "^0.149.0",
|
||||
"flow-bin": "^0.162.0",
|
||||
"fluent_conv": "^3.1.0",
|
||||
"glob": "^7.2.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^26.6.3",
|
||||
"metro-react-native-babel-preset": "^0.66.0",
|
||||
"metro-react-native-babel-preset": "^0.66.2",
|
||||
"nock": "^13.2.2",
|
||||
"react-native-accessibility-engine": "^1.0.0",
|
||||
"react-native-clean-project": "^3.6.7",
|
||||
"react-native-clean-project": "^4.0.1",
|
||||
"react-native-codegen": "^0.0.7",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"yargs": "^17.3.1"
|
||||
|
||||
26
src/components/About.js
Normal file
26
src/components/About.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Text
|
||||
} from "react-native";
|
||||
import { getVersion, getBuildNumber } from "react-native-device-info";
|
||||
|
||||
import type { Node } from "react";
|
||||
import ViewWithFooter from "./SharedComponents/ViewWithFooter";
|
||||
|
||||
const AboutScreen = ( ): Node => {
|
||||
const appVersion = getVersion( );
|
||||
const buildVersion = getBuildNumber( );
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<Text>
|
||||
app version:
|
||||
{` ${appVersion} (${buildVersion})`}
|
||||
</Text>
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutScreen;
|
||||
@@ -4,8 +4,9 @@ import * as React from "react";
|
||||
import { Text, View, Pressable } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import { textStyles } from "../../styles/sharedComponents/modal";
|
||||
import { textStyles, viewStyles } from "../../styles/sharedComponents/modal";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
|
||||
type Props = {
|
||||
closeModal: ( ) => void
|
||||
@@ -39,6 +40,15 @@ const CameraOptionsModal = ( { closeModal }: Props ): React.Node => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TranslatedText style={textStyles.whiteText} text="CREATE-AN-OBSERVATION" />
|
||||
<View style={viewStyles.whiteModal}>
|
||||
<TranslatedText text="STEP-1-EVIDENCE" />
|
||||
<TranslatedText text="The-first-thing-you-need-is-evidence" />
|
||||
<TranslatedText text="Take-a-photo-with-your-camera" />
|
||||
<TranslatedText text="Upload-a-photo-from-your-gallery" />
|
||||
<TranslatedText text="Record-a-sound" />
|
||||
<TranslatedText text="Submit-without-evidence" />
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={navToNormalCamera}
|
||||
>
|
||||
|
||||
105
src/components/Camera/CameraView.js
Normal file
105
src/components/Camera/CameraView.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// @flow
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { StyleSheet, Animated } from "react-native";
|
||||
import { Camera } from "react-native-vision-camera";
|
||||
import type { Node } from "react";
|
||||
import { useIsFocused } from "@react-navigation/core";
|
||||
import { PinchGestureHandler, TapGestureHandler } from "react-native-gesture-handler";
|
||||
import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from "react-native-reanimated";
|
||||
|
||||
import FocusSquare from "./FocusSquare";
|
||||
import { useIsForeground } from "./hooks/useIsForeground";
|
||||
|
||||
// a lot of the camera functionality (pinch to zoom, etc.) is lifted from the example library:
|
||||
// https://github.com/mrousavy/react-native-vision-camera/blob/7335883969c9102b8a6d14ca7ed871f3de7e1389/example/src/CameraPage.tsx
|
||||
|
||||
const SCALE_FULL_ZOOM = 3;
|
||||
|
||||
const ReanimatedCamera = Reanimated.createAnimatedComponent( Camera );
|
||||
Reanimated.addWhitelistedNativeProps( {
|
||||
zoom: true
|
||||
} );
|
||||
|
||||
type Props = {
|
||||
camera: Object,
|
||||
device: Object
|
||||
}
|
||||
|
||||
const CameraView = ( { camera, device }: Props ): Node => {
|
||||
const zoom = useSharedValue( 0 );
|
||||
const [tappedCoordinates, setTappedCoordinates] = useState( null );
|
||||
const tapToFocusAnimation = useRef( new Animated.Value( 0 ) ).current;
|
||||
// check if camera page is active
|
||||
const isFocused = useIsFocused( );
|
||||
const isForeground = useIsForeground( );
|
||||
const isActive = isFocused && isForeground;
|
||||
|
||||
const minZoom = device?.minZoom ?? 1;
|
||||
const maxZoom = Math.min( device?.maxZoom ?? 1, 5 );
|
||||
|
||||
const cameraAnimatedProps = useAnimatedProps( () => {
|
||||
const z = Math.max( Math.min( zoom.value, maxZoom ), minZoom );
|
||||
return {
|
||||
zoom: z
|
||||
};
|
||||
}, [maxZoom, minZoom, zoom] );
|
||||
//#endregion
|
||||
|
||||
//#region Pinch to Zoom Gesture
|
||||
// The gesture handler maps the linear pinch gesture (0 - 1) to an exponential curve since a camera's zoom
|
||||
// function does not appear linear to the user. (aka zoom 0.1 -> 0.2 does not look equal in difference as 0.8 -> 0.9)
|
||||
const onPinchGesture = useAnimatedGestureHandler( {
|
||||
onStart: ( _, context ) => {
|
||||
context.startZoom = zoom.value;
|
||||
},
|
||||
onActive: ( event, context ) => {
|
||||
// we're trying to map the scale gesture to a linear zoom here
|
||||
const startZoom = context.startZoom ?? 0;
|
||||
const scale = interpolate( event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP );
|
||||
zoom.value = interpolate( scale, [-1, 0, 1], [minZoom, startZoom, maxZoom], Extrapolate.CLAMP );
|
||||
}
|
||||
} );
|
||||
//#endregion
|
||||
|
||||
//#region Effects
|
||||
const neutralZoom = device?.neutralZoom ?? 1;
|
||||
useEffect( ( ) => {
|
||||
// Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
|
||||
zoom.value = neutralZoom;
|
||||
}, [neutralZoom, zoom] );
|
||||
|
||||
const tapToFocus = async ( { nativeEvent } ) => {
|
||||
try {
|
||||
await camera.current.focus( { x: nativeEvent.x, y: nativeEvent.y } );
|
||||
tapToFocusAnimation.setValue( 1 );
|
||||
setTappedCoordinates( nativeEvent );
|
||||
} catch ( e ) {
|
||||
console.log( e, "couldn't tap to focus" );
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}>
|
||||
<Reanimated.View style={StyleSheet.absoluteFill}>
|
||||
<TapGestureHandler onHandlerStateChange={tapToFocus} numberOfTaps={1}>
|
||||
<ReanimatedCamera
|
||||
ref={camera}
|
||||
style={StyleSheet.absoluteFill}
|
||||
device={device}
|
||||
isActive={isActive}
|
||||
photo
|
||||
animatedProps={cameraAnimatedProps}
|
||||
/>
|
||||
</TapGestureHandler>
|
||||
</Reanimated.View>
|
||||
</PinchGestureHandler>
|
||||
<FocusSquare
|
||||
tapToFocusAnimation={tapToFocusAnimation}
|
||||
tappedCoordinates={tappedCoordinates}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CameraView;
|
||||
@@ -1,32 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import React, { useRef, useState, useEffect, useContext } from "react";
|
||||
import { Text, StyleSheet, View, Pressable, Animated, Image } from "react-native";
|
||||
import { Text, View, Pressable, Platform } from "react-native";
|
||||
import { Camera, useCameraDevices } from "react-native-vision-camera";
|
||||
import type { Node } from "react";
|
||||
import { FlatList, PinchGestureHandler, TapGestureHandler } from "react-native-gesture-handler";
|
||||
import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from "react-native-reanimated";
|
||||
import { useIsFocused } from "@react-navigation/core";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import uuid from "react-native-uuid";
|
||||
|
||||
import { useUserLocation } from "../../sharedHooks/useUserLocation";
|
||||
|
||||
import { viewStyles, imageStyles } from "../../styles/camera/normalCamera";
|
||||
import { useIsForeground } from "./hooks/useIsForeground";
|
||||
import FocusSquare from "./FocusSquare";
|
||||
import { viewStyles, textStyles } from "../../styles/camera/normalCamera";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
|
||||
// a lot of the camera functionality (pinch to zoom, etc.) is lifted from the example library:
|
||||
// https://github.com/mrousavy/react-native-vision-camera/blob/7335883969c9102b8a6d14ca7ed871f3de7e1389/example/src/CameraPage.tsx
|
||||
|
||||
const SCALE_FULL_ZOOM = 3;
|
||||
|
||||
const ReanimatedCamera = Reanimated.createAnimatedComponent( Camera );
|
||||
Reanimated.addWhitelistedNativeProps( {
|
||||
zoom: true
|
||||
} );
|
||||
import CameraView from "./CameraView";
|
||||
import TopPhotos from "./TopPhotos";
|
||||
import checkCameraPermissions from "./helpers/androidPermissions";
|
||||
|
||||
const NormalCamera = ( ): Node => {
|
||||
const [permission, setPermission] = useState( null );
|
||||
const { addPhotos } = useContext( ObsEditContext );
|
||||
const latLng = useUserLocation( );
|
||||
const latitude = latLng && latLng.latitude;
|
||||
@@ -40,43 +29,33 @@ const NormalCamera = ( ): Node => {
|
||||
const [takePhotoOptions, setTakePhotoOptions] = useState( {
|
||||
flash: "off"
|
||||
} );
|
||||
const zoom = useSharedValue( 0 );
|
||||
const [tappedCoordinates, setTappedCoordinates] = useState( null );
|
||||
const tapToFocusAnimation = useRef( new Animated.Value( 0 ) ).current;
|
||||
const [observationPhotos, setObservationPhotos] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
navigation.addListener( "focus", async ( ) => {
|
||||
const cameraPermission = await Camera.getCameraPermissionStatus( );
|
||||
if ( cameraPermission === "not-determined" ) {
|
||||
await Camera.requestCameraPermission( );
|
||||
const requestAndroidPermissions = ( ) => {
|
||||
checkCameraPermissions( ).then( ( result ) => {
|
||||
if ( result === "permissions" ) {
|
||||
console.log( result, "result in then catch" );
|
||||
}
|
||||
console.log( "result not permissions" );
|
||||
} ).catch( e => console.log( e, "couldn't get camera permissions" ) );
|
||||
};
|
||||
|
||||
navigation.addListener( "focus", ( ) => {
|
||||
if ( Platform.OS === "android" ) {
|
||||
console.log( "requesting android permissions on focus" );
|
||||
requestAndroidPermissions( );
|
||||
}
|
||||
} );
|
||||
}, [navigation] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
navigation.addListener( "blur", ( ) => {
|
||||
if ( observationPhotos.length > 0 ) {
|
||||
setObservationPhotos( [] );
|
||||
}
|
||||
} );
|
||||
}, [navigation, observationPhotos.length] );
|
||||
|
||||
// check if camera page is active
|
||||
const isFocused = useIsFocused( );
|
||||
const isForeground = useIsForeground( );
|
||||
const isActive = isFocused && isForeground;
|
||||
|
||||
const minZoom = device?.minZoom ?? 1;
|
||||
const maxZoom = Math.min( device?.maxZoom ?? 1, 5 );
|
||||
|
||||
const cameraAnimatedProps = useAnimatedProps( () => {
|
||||
const z = Math.max( Math.min( zoom.value, maxZoom ), minZoom );
|
||||
return {
|
||||
zoom: z
|
||||
};
|
||||
}, [maxZoom, minZoom, zoom] );
|
||||
//#endregion
|
||||
|
||||
// select different devices
|
||||
// front or back camera
|
||||
// tap to focus
|
||||
// zoom in
|
||||
}, [navigation, observationPhotos] );
|
||||
|
||||
const takePhoto = async ( ) => {
|
||||
try {
|
||||
@@ -113,77 +92,25 @@ const NormalCamera = ( ): Node => {
|
||||
setCameraPosition( newPosition );
|
||||
};
|
||||
|
||||
//#region Effects
|
||||
const neutralZoom = device?.neutralZoom ?? 1;
|
||||
useEffect( ( ) => {
|
||||
// Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
|
||||
zoom.value = neutralZoom;
|
||||
}, [neutralZoom, zoom] );
|
||||
|
||||
//#region Pinch to Zoom Gesture
|
||||
// The gesture handler maps the linear pinch gesture (0 - 1) to an exponential curve since a camera's zoom
|
||||
// function does not appear linear to the user. (aka zoom 0.1 -> 0.2 does not look equal in difference as 0.8 -> 0.9)
|
||||
const onPinchGesture = useAnimatedGestureHandler( {
|
||||
onStart: ( _, context ) => {
|
||||
context.startZoom = zoom.value;
|
||||
},
|
||||
onActive: ( event, context ) => {
|
||||
// we're trying to map the scale gesture to a linear zoom here
|
||||
const startZoom = context.startZoom ?? 0;
|
||||
const scale = interpolate( event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP );
|
||||
zoom.value = interpolate( scale, [-1, 0, 1], [minZoom, startZoom, maxZoom], Extrapolate.CLAMP );
|
||||
}
|
||||
} );
|
||||
//#endregion
|
||||
|
||||
const tapToFocus = async ( { nativeEvent } ) => {
|
||||
try {
|
||||
await camera.current.focus( { x: nativeEvent.x, y: nativeEvent.y } );
|
||||
tapToFocusAnimation.setValue( 1 );
|
||||
setTappedCoordinates( nativeEvent );
|
||||
} catch ( e ) {
|
||||
console.log( e, "couldn't tap to focus" );
|
||||
}
|
||||
};
|
||||
|
||||
const renderSmallPhoto = ( { item } ) => (
|
||||
<Image source={{ uri: item.uri }} style={imageStyles.smallPhoto} />
|
||||
);
|
||||
|
||||
const navToObsEdit = ( ) => {
|
||||
addPhotos( observationPhotos );
|
||||
navigation.navigate( "ObsEdit" );
|
||||
};
|
||||
|
||||
if ( device == null ) { return null;}
|
||||
console.log( device === null, permission, "device and permission" );
|
||||
|
||||
// $FlowFixMe
|
||||
if ( permission === "denied" ) {
|
||||
return (
|
||||
<View style={viewStyles.container}>
|
||||
<Text style={textStyles.whiteText}>check camera permissions in phone settings</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={viewStyles.container}>
|
||||
{device !== null && (
|
||||
<PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}>
|
||||
<Reanimated.View style={StyleSheet.absoluteFill}>
|
||||
<TapGestureHandler onHandlerStateChange={tapToFocus} numberOfTaps={1}>
|
||||
<ReanimatedCamera
|
||||
ref={camera}
|
||||
style={StyleSheet.absoluteFill}
|
||||
device={device}
|
||||
isActive={isActive}
|
||||
photo
|
||||
animatedProps={cameraAnimatedProps}
|
||||
/>
|
||||
</TapGestureHandler>
|
||||
</Reanimated.View>
|
||||
</PinchGestureHandler>
|
||||
)}
|
||||
<FlatList
|
||||
data={observationPhotos}
|
||||
contentContainerStyle={viewStyles.photoContainer}
|
||||
renderItem={renderSmallPhoto}
|
||||
horizontal
|
||||
/>
|
||||
<FocusSquare
|
||||
tapToFocusAnimation={tapToFocusAnimation}
|
||||
tappedCoordinates={tappedCoordinates}
|
||||
/>
|
||||
{device && <CameraView device={device} camera={camera} />}
|
||||
<TopPhotos observationPhotos={observationPhotos} />
|
||||
<View style={viewStyles.row}>
|
||||
<Pressable
|
||||
style={viewStyles.flashButton}
|
||||
|
||||
28
src/components/Camera/TopPhotos.js
Normal file
28
src/components/Camera/TopPhotos.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { FlatList, Image } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { viewStyles, imageStyles } from "../../styles/camera/normalCamera";
|
||||
|
||||
type Props = {
|
||||
observationPhotos: Array<Object>
|
||||
}
|
||||
|
||||
const TopPhotos = ( { observationPhotos }: Props ): Node => {
|
||||
const renderSmallPhoto = ( { item } ) => (
|
||||
<Image source={{ uri: item.uri }} style={imageStyles.smallPhoto} />
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={observationPhotos}
|
||||
contentContainerStyle={viewStyles.photoContainer}
|
||||
renderItem={renderSmallPhoto}
|
||||
horizontal
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopPhotos;
|
||||
21
src/components/Camera/helpers/androidPermissions.js
Normal file
21
src/components/Camera/helpers/androidPermissions.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import { PermissionsAndroid } from "react-native";
|
||||
|
||||
const checkCameraPermissions = async ( ): Promise<any> => {
|
||||
const { PERMISSIONS, RESULTS } = PermissionsAndroid;
|
||||
|
||||
try {
|
||||
const granted = await PermissionsAndroid.request( PERMISSIONS.CAMERA );
|
||||
console.log( granted, "granted camera permissions in helper func" );
|
||||
|
||||
if ( granted === RESULTS.GRANTED ) {
|
||||
return true;
|
||||
}
|
||||
return "permissions";
|
||||
} catch ( e ) {
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
export default checkCameraPermissions;
|
||||
@@ -35,7 +35,7 @@ const CustomDrawerContent = ( { ...props }: Props ): Node => {
|
||||
/>
|
||||
<DrawerItem
|
||||
label="about"
|
||||
onPress={( ) => console.log( "nav to about" )}
|
||||
onPress={( ) => navigation.navigate( "about" )}
|
||||
/>
|
||||
<DrawerItem
|
||||
label="help/tutorials"
|
||||
@@ -49,6 +49,10 @@ const CustomDrawerContent = ( { ...props }: Props ): Node => {
|
||||
label="network/logging"
|
||||
onPress={( ) => navigation.navigate( "network" )}
|
||||
/>
|
||||
<DrawerItem
|
||||
label="projects"
|
||||
onPress={( ) => navigation.navigate( "projects" )}
|
||||
/>
|
||||
</DrawerContentScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
63
src/components/Explore/BottomCard.js
Normal file
63
src/components/Explore/BottomCard.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import type { Node } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/explore/explore";
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import FiltersIcon from "./FiltersIcon";
|
||||
|
||||
const Explore = ( ): Node => {
|
||||
const {
|
||||
exploreFilters,
|
||||
setExploreFilters,
|
||||
taxon,
|
||||
setTaxon,
|
||||
location,
|
||||
setLocation
|
||||
} = useContext( ExploreContext );
|
||||
const setTaxonId = ( getValue ) => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
taxon_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const setPlaceId = ( getValue ) => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
place_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const taxonId = exploreFilters ? exploreFilters.taxon_id : null;
|
||||
const placeId = exploreFilters ? exploreFilters.place_id : null;
|
||||
|
||||
return (
|
||||
<View style={viewStyles.bottomCard}>
|
||||
<TranslatedText text="Explore" />
|
||||
<FiltersIcon />
|
||||
<DropdownPicker
|
||||
searchQuery={taxon}
|
||||
setSearchQuery={setTaxon}
|
||||
setValue={setTaxonId}
|
||||
sources="taxa"
|
||||
value={taxonId}
|
||||
placeholder="Search-for-a-taxon"
|
||||
/>
|
||||
<DropdownPicker
|
||||
searchQuery={location}
|
||||
setSearchQuery={setLocation}
|
||||
setValue={setPlaceId}
|
||||
sources="places"
|
||||
value={placeId}
|
||||
placeholder="Search-for-a-location"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Explore;
|
||||
@@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { Text, Pressable } from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/observations/messagesIcon";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
|
||||
const ClearFiltersButton = ( ): React.Node => {
|
||||
const { clearFilters } = React.useContext( ExploreContext );
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={clearFilters}
|
||||
style={viewStyles.messages}
|
||||
>
|
||||
<Text>clear</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearFiltersButton;
|
||||
@@ -7,6 +7,7 @@ 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 { t } from "i18next";
|
||||
|
||||
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { imageStyles, viewStyles } from "../../styles/explore/explore";
|
||||
@@ -16,7 +17,8 @@ type Props = {
|
||||
setSearchQuery: string => { },
|
||||
setValue: number => { },
|
||||
sources: string,
|
||||
value: number
|
||||
value: number,
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
const DropdownPicker = ( {
|
||||
@@ -24,7 +26,8 @@ const DropdownPicker = ( {
|
||||
setSearchQuery,
|
||||
setValue,
|
||||
sources,
|
||||
value
|
||||
value,
|
||||
placeholder
|
||||
}: Props ): Node => {
|
||||
const searchResults = useRemoteSearchResults( searchQuery, sources );
|
||||
|
||||
@@ -85,7 +88,7 @@ const DropdownPicker = ( {
|
||||
searchable={true}
|
||||
disableLocalSearch={true}
|
||||
onChangeSearchText={setSearchQuery}
|
||||
placeholder="Search"
|
||||
placeholder={t( placeholder )}
|
||||
style={viewStyles.dropdown}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,77 +1,41 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useContext } from "react";
|
||||
import { Text } from "react-native";
|
||||
import React, { useContext } from "react";
|
||||
import type { Node } from "react";
|
||||
import { Dimensions } from "react-native";
|
||||
|
||||
import { textStyles } from "../../styles/explore/explore";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import ObservationViews from "../SharedComponents/ObservationViews/ObservationViews";
|
||||
import BottomCard from "./BottomCard";
|
||||
|
||||
const { height } = Dimensions.get( "screen" );
|
||||
|
||||
// make map small enough to show bottom card
|
||||
const mapHeight = height - 450;
|
||||
|
||||
const Explore = ( ): Node => {
|
||||
const {
|
||||
exploreList,
|
||||
loadingExplore,
|
||||
setLoading,
|
||||
exploreFilters,
|
||||
setExploreFilters
|
||||
totalObservations
|
||||
} = useContext( ExploreContext );
|
||||
const [taxon, setTaxon] = useState( "" );
|
||||
const [location, setLocation] = useState( "" );
|
||||
|
||||
const showMap = ( ) => setLoading( );
|
||||
|
||||
const setTaxonId = ( getValue ) => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
taxon_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const setPlaceId = ( getValue ) => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
place_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const taxonId = exploreFilters ? exploreFilters.taxon_id : null;
|
||||
const placeId = exploreFilters ? exploreFilters.place_id : null;
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<Text style={textStyles.explanation}>search for species and taxa seen anywhere in the world</Text>
|
||||
<Text style={textStyles.explanation}>try searching for insects near your location...</Text>
|
||||
<DropdownPicker
|
||||
searchQuery={taxon}
|
||||
setSearchQuery={setTaxon}
|
||||
setValue={setTaxonId}
|
||||
sources="taxa"
|
||||
value={taxonId}
|
||||
/>
|
||||
<DropdownPicker
|
||||
searchQuery={location}
|
||||
setSearchQuery={setLocation}
|
||||
setValue={setPlaceId}
|
||||
sources="places"
|
||||
value={placeId}
|
||||
/>
|
||||
<RoundGreenButton
|
||||
buttonText="EXPLORE ORGANISMS"
|
||||
handlePress={showMap}
|
||||
testID="Explore.fetchObservations"
|
||||
/>
|
||||
{taxonId !== null && (
|
||||
<ObservationViews
|
||||
loading={loadingExplore}
|
||||
observationList={exploreList}
|
||||
taxonId={taxonId}
|
||||
testID="Explore.observations"
|
||||
mapHeight={mapHeight}
|
||||
totalObservations={totalObservations}
|
||||
/>
|
||||
)}
|
||||
<BottomCard />
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,126 +1,428 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useContext } from "react";
|
||||
import { Text } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import RNPickerSelect from "react-native-picker-select";
|
||||
import type { Node } from "react";
|
||||
import CheckBox from "@react-native-community/checkbox";
|
||||
import RadioButtonRN from "radio-buttons-react-native";
|
||||
import { t } from "i18next";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import { pickerSelectStyles } from "../../styles/explore/exploreFilters";
|
||||
import { pickerSelectStyles, viewStyles } from "../../styles/explore/exploreFilters";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import TaxonLocationSearch from "./TaxonLocationSearch";
|
||||
import ScrollNoFooter from "../SharedComponents/ScrollNoFooter";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import ExploreFooter from "./ExploreFooter";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import ResetFiltersButton from "./ResetFiltersButton";
|
||||
|
||||
const ExploreFilters = ( ): Node => {
|
||||
const [project, setProject] = useState( "" );
|
||||
const [user, setUser] = useState( "" );
|
||||
const { exploreFilters, setExploreFilters } = useContext( ExploreContext );
|
||||
const {
|
||||
exploreFilters,
|
||||
setExploreFilters,
|
||||
unappliedFilters,
|
||||
setUnappliedFilters
|
||||
} = useContext( ExploreContext );
|
||||
|
||||
const setProjectId = ( getValue ) => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
project_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const setUserId = ( getValue ) => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
user_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const sortByRadioButtons = [{
|
||||
label: t( "Date-added-newest-to-oldest" ),
|
||||
type: "desc"
|
||||
}, {
|
||||
label: t( "Date-added-oldest-to-newest" ),
|
||||
type: "asc"
|
||||
}, {
|
||||
label: t( "Recently-observed" ),
|
||||
type: "observed_on"
|
||||
}, {
|
||||
label: t( "Most-faved" ),
|
||||
type: "votes"
|
||||
}];
|
||||
|
||||
const reviewedRadioButtons = [{
|
||||
label: t( "All-observations" ),
|
||||
type: "all"
|
||||
}, {
|
||||
label: t( "Reviewed-only" ),
|
||||
type: "reviewed"
|
||||
}, {
|
||||
label: t( "Unreviewed-only" ),
|
||||
type: "unreviewed"
|
||||
}];
|
||||
|
||||
const months = [
|
||||
{ label: "jan", value: 1 },
|
||||
{ label: "feb", value: 2 },
|
||||
{ label: "mar", value: 3 },
|
||||
{ label: "apr", value: 4 },
|
||||
{ label: "may", value: 5 },
|
||||
{ label: "jun", value: 6 },
|
||||
{ label: "jul", value: 7 },
|
||||
{ label: "aug", value: 8 },
|
||||
{ label: "sept", value: 9 },
|
||||
{ label: "oct", value: 10 },
|
||||
{ label: "nov", value: 11 },
|
||||
{ label: "dec", value: 12 }
|
||||
{ label: t( "Month-January" ), value: 1 },
|
||||
{ label: t( "Month-February" ), value: 2 },
|
||||
{ label: t( "Month-March" ), value: 3 },
|
||||
{ label: t( "Month-April" ), value: 4 },
|
||||
{ label: t( "Month-May" ), value: 5 },
|
||||
{ label: t( "Month-June" ), value: 6 },
|
||||
{ label: t( "Month-July" ), value: 7 },
|
||||
{ label: t( "Month-August" ), value: 8 },
|
||||
{ label: t( "Month-September" ), value: 9 },
|
||||
{ label: t( "Month-October" ), value: 10 },
|
||||
{ label: t( "Month-November" ), value: 11 },
|
||||
{ label: t( "Month-December" ), value: 12 }
|
||||
];
|
||||
|
||||
const qualityGradeOptions = [
|
||||
{ label: "research", value: "research" },
|
||||
{ label: "needs id", value: "needs_id" }
|
||||
const photoLicenses = [
|
||||
{ label: t( "All" ), value: "all" },
|
||||
{ label: "CC-BY", value: "cc-by" },
|
||||
{ label: "CC-BY-NC", value: "cc-by-nc" },
|
||||
{ label: "CC-BY-ND", value: "cc-by-nd" },
|
||||
{ label: "CC-BY-SA", value: "cc-by-sa" },
|
||||
{ label: "CC-BY-NC-ND", value: "cc-by-nc-nd" },
|
||||
{ label: "CC-BY-NC-SA", value: "cc-by-nc-sa" },
|
||||
{ label: "CC0", value: "cc0" }
|
||||
];
|
||||
|
||||
const sortOptions = [
|
||||
{ label: "id", value: "observations.id" },
|
||||
{ label: "observed on", value: "observed_on" },
|
||||
{ label: "faves", value: "votes" }
|
||||
const ranks = [
|
||||
{ label: t( "Ranks-stateofmatter" ), value: "stateofmatter" },
|
||||
{ label: t( "Ranks-kingdom" ), value: "kingdom" },
|
||||
{ label: t( "Ranks-subkingdom" ), value: "subkingdom" },
|
||||
{ label: t( "Ranks-phylum" ), value: "phylum" },
|
||||
{ label: t( "Ranks-subphylum" ), value: "subphylum" },
|
||||
{ label: t( "Ranks-superclass" ), value: "superclass" },
|
||||
{ label: t( "Ranks-class" ), value: "class" },
|
||||
{ label: t( "Ranks-subclass" ), value: "subclass" },
|
||||
{ label: t( "Ranks-infraclass" ), value: "infraclass" },
|
||||
{ label: t( "Ranks-superorder" ), value: "superorder" },
|
||||
{ label: t( "Ranks-order" ), value: "order" },
|
||||
{ label: t( "Ranks-suborder" ), value: "suborder" },
|
||||
{ label: t( "Ranks-infraorder" ), value: "infraorder" },
|
||||
{ label: t( "Ranks-subterclass" ), value: "subterclass" },
|
||||
{ label: t( "Ranks-parvorder" ), value: "parvorder" },
|
||||
{ label: t( "Ranks-zoosection" ), value: "zoosection" },
|
||||
{ label: t( "Ranks-zoosubsection" ), value: "zoosubsection" },
|
||||
{ label: t( "Ranks-superfamily" ), value: "superfamily" },
|
||||
{ label: t( "Ranks-epifamily" ), value: "epifamily" },
|
||||
{ label: t( "Ranks-family" ), value: "family" },
|
||||
{ label: t( "Ranks-subfamily" ), value: "subfamily" },
|
||||
{ label: t( "Ranks-supertribe" ), value: "supertribe" },
|
||||
{ label: t( "Ranks-tribe" ), value: "tribe" },
|
||||
{ label: t( "Ranks-subtribe" ), value: "subtribe" },
|
||||
{ label: t( "Ranks-genus" ), value: "genus" },
|
||||
{ label: t( "Ranks-genushybrid" ), value: "genushybrid" },
|
||||
{ label: t( "Ranks-subgenus" ), value: "subgenus" },
|
||||
{ label: t( "Ranks-section" ), value: "section" },
|
||||
{ label: t( "Ranks-subsection" ), value: "subsection" },
|
||||
{ label: t( "Ranks-complex" ), value: "complex" },
|
||||
{ label: t( "Ranks-species" ), value: "species" },
|
||||
{ label: t( "Ranks-hybrid" ), value: "hybrid" },
|
||||
{ label: t( "Ranks-subspecies" ), value: "subspecies" },
|
||||
{ label: t( "Ranks-variety" ), value: "variety" },
|
||||
{ label: t( "Ranks-form" ), value: "form" },
|
||||
{ label: t( "Ranks-infrahybrid" ), value: "infrahybrid" }
|
||||
];
|
||||
|
||||
const projectId = exploreFilters ? exploreFilters.project_id : null;
|
||||
const userId = exploreFilters ? exploreFilters.user_id : null;
|
||||
const projectId = unappliedFilters ? unappliedFilters.project_id : null;
|
||||
const userId = unappliedFilters ? unappliedFilters.user_id : null;
|
||||
|
||||
const renderQualityGradeCheckbox = ( qualityGrade ) => {
|
||||
const filter = unappliedFilters.quality_grade;
|
||||
const hasFilter = filter.includes( qualityGrade );
|
||||
|
||||
return (
|
||||
<CheckBox
|
||||
boxType="square"
|
||||
disabled={false}
|
||||
value={hasFilter}
|
||||
onValueChange={( ) => {
|
||||
if ( hasFilter ) {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
quality_grade: filter.filter( e => e !== qualityGrade )
|
||||
} );
|
||||
} else {
|
||||
filter.push( qualityGrade );
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
quality_grade: filter
|
||||
} );
|
||||
}
|
||||
}}
|
||||
style={viewStyles.checkbox}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMediaCheckbox = ( mediaType ) => {
|
||||
const { sounds, photos } = unappliedFilters;
|
||||
return (
|
||||
<CheckBox
|
||||
boxType="square"
|
||||
disabled={false}
|
||||
value={mediaType === "photos" ? photos : sounds}
|
||||
onValueChange={( ) => {
|
||||
if ( mediaType === "photos" ) {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
photos: !unappliedFilters.photos
|
||||
} );
|
||||
} else {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
sounds: !unappliedFilters.sounds
|
||||
} );
|
||||
}
|
||||
}}
|
||||
style={viewStyles.checkbox}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatusCheckbox = ( status ) => {
|
||||
const { native, captive, introduced, threatened } = unappliedFilters;
|
||||
|
||||
let value;
|
||||
|
||||
if ( status === "native" ) {
|
||||
value = native;
|
||||
} else if ( status === "captive" ) {
|
||||
value = captive;
|
||||
} else if ( status === "introduced" ) {
|
||||
value = introduced;
|
||||
} else {
|
||||
value = threatened;
|
||||
}
|
||||
|
||||
return (
|
||||
<CheckBox
|
||||
boxType="square"
|
||||
disabled={false}
|
||||
value={value}
|
||||
onValueChange={( ) => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
// $FlowFixMe
|
||||
[status]: !unappliedFilters[status]
|
||||
} );
|
||||
}}
|
||||
style={viewStyles.checkbox}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRankPicker = ( rank ) => (
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
// $FlowFixMe
|
||||
[rank]: [itemValue]
|
||||
} );
|
||||
}}
|
||||
items={ranks}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={unappliedFilters[rank].length > 0 ? unappliedFilters[rank][0] : null}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderMonthsPicker = ( ) => {
|
||||
const firstMonth = unappliedFilters.months[0];
|
||||
const lastMonth = unappliedFilters.months[unappliedFilters.months.length - 1];
|
||||
|
||||
const includesMonth = value => unappliedFilters.months.includes( value );
|
||||
|
||||
const fillInMonths = ( itemValue ) => {
|
||||
months.forEach( ( { value } ) => {
|
||||
if ( value >= firstMonth && value <= itemValue && !includesMonth( value ) ) {
|
||||
unappliedFilters.months.push( value );
|
||||
} else if ( value > itemValue && includesMonth( value ) ) {
|
||||
const index = unappliedFilters.months.indexOf( value );
|
||||
unappliedFilters.months.splice( index );
|
||||
}
|
||||
} );
|
||||
setUnappliedFilters( { ...unappliedFilters } );
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) => {
|
||||
unappliedFilters.months = [itemValue];
|
||||
setUnappliedFilters( { ...unappliedFilters } );
|
||||
}}
|
||||
items={months}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={firstMonth}
|
||||
/>
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) => fillInMonths( itemValue )}
|
||||
items={months}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={lastMonth}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<Text>FILTER BY</Text>
|
||||
<Text>user</Text>
|
||||
<DropdownPicker
|
||||
searchQuery={user}
|
||||
setSearchQuery={setUser}
|
||||
setValue={setUserId}
|
||||
sources="users"
|
||||
value={userId}
|
||||
/>
|
||||
<Text>project</Text>
|
||||
<DropdownPicker
|
||||
searchQuery={project}
|
||||
setSearchQuery={setProject}
|
||||
setValue={setProjectId}
|
||||
sources="projects"
|
||||
value={projectId}
|
||||
/>
|
||||
<Text>quality grade</Text>
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) =>
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
quality_grade: itemValue
|
||||
} )
|
||||
}
|
||||
items={qualityGradeOptions}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={exploreFilters.quality_grade}
|
||||
/>
|
||||
<Text>status</Text>
|
||||
{/* TODO: not sure what goes here. maybe threatened, introduced, captive, wild? */}
|
||||
<Text>date</Text>
|
||||
<Text>months</Text>
|
||||
{/* TODO: make months accept multiple values */}
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) =>
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
month: itemValue
|
||||
} )
|
||||
}
|
||||
items={months}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={exploreFilters.month}
|
||||
/>
|
||||
<Text>sort by</Text>
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) =>
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
sort_by: itemValue
|
||||
} )
|
||||
}
|
||||
items={sortOptions}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={exploreFilters.sort_by}
|
||||
/>
|
||||
</ViewWithFooter>
|
||||
<>
|
||||
<ScrollNoFooter>
|
||||
<TaxonLocationSearch />
|
||||
<TranslatedText text="Sort-by" />
|
||||
<RadioButtonRN
|
||||
data={sortByRadioButtons}
|
||||
initial={1}
|
||||
boxStyle={viewStyles.radioButtonBox}
|
||||
selectedBtn={( { type } ) => {
|
||||
if ( type === "desc" || type === "asc" ) {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
order: type,
|
||||
order_by: "created_at"
|
||||
} );
|
||||
} else {
|
||||
// votes or observed_on only sort by most recent
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
order: "desc",
|
||||
order_by: type
|
||||
} );
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<View style={viewStyles.filtersRow}>
|
||||
<TranslatedText text="Filters" />
|
||||
<ResetFiltersButton />
|
||||
</View>
|
||||
<TranslatedText text="Quality-Grade" />
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderQualityGradeCheckbox( "research" )}
|
||||
<TranslatedText text="Research-Grade" />
|
||||
</View>
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderQualityGradeCheckbox( "needs_id" )}
|
||||
<TranslatedText text="Needs-ID" />
|
||||
</View>
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderQualityGradeCheckbox( "casual" )}
|
||||
<TranslatedText text="Casual" />
|
||||
</View>
|
||||
<TranslatedText text="User" />
|
||||
<TranslatedText text="Search-for-a-user" />
|
||||
<DropdownPicker
|
||||
searchQuery={user}
|
||||
setSearchQuery={setUser}
|
||||
setValue={setUserId}
|
||||
sources="users"
|
||||
value={userId}
|
||||
/>
|
||||
<TranslatedText text="Projects" />
|
||||
<TranslatedText text="Search-for-a-project" />
|
||||
<DropdownPicker
|
||||
searchQuery={project}
|
||||
setSearchQuery={setProject}
|
||||
setValue={setProjectId}
|
||||
sources="projects"
|
||||
value={projectId}
|
||||
/>
|
||||
<TranslatedText text="Rank" />
|
||||
<TranslatedText text="Low" />
|
||||
{renderRankPicker( "lrank" )}
|
||||
<TranslatedText text="High" />
|
||||
{renderRankPicker( "hrank" )}
|
||||
<TranslatedText text="Date" />
|
||||
<TranslatedText text="Months" />
|
||||
{renderMonthsPicker( )}
|
||||
<TranslatedText text="Media" />
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderMediaCheckbox( "photos" )}
|
||||
<TranslatedText text="Has-Photos" />
|
||||
</View>
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderMediaCheckbox( "sounds" )}
|
||||
<TranslatedText text="Has-Sounds" />
|
||||
</View>
|
||||
<TranslatedText text="Status" />
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderStatusCheckbox( "introduced" )}
|
||||
<TranslatedText text="Introduced" />
|
||||
</View>
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderStatusCheckbox( "native" )}
|
||||
<TranslatedText text="Native" />
|
||||
</View>
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderStatusCheckbox( "threatened" )}
|
||||
<TranslatedText text="Threatened" />
|
||||
</View>
|
||||
<View style={viewStyles.checkboxRow}>
|
||||
{renderStatusCheckbox( "captive" )}
|
||||
<TranslatedText text="Captive-Cultivated" />
|
||||
</View>
|
||||
<TranslatedText text="Reviewed" />
|
||||
<RadioButtonRN
|
||||
data={reviewedRadioButtons}
|
||||
initial={1}
|
||||
boxStyle={viewStyles.radioButtonBox}
|
||||
selectedBtn={( { type } ) => {
|
||||
if ( type === "all" ) {
|
||||
delete unappliedFilters.reviewed;
|
||||
setUnappliedFilters( { ...unappliedFilters } );
|
||||
} else if ( type === "reviewed" ) {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
reviewed: true
|
||||
} );
|
||||
} else {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
reviewed: false
|
||||
} );
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TranslatedText text="Photo-Licensing" />
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
photo_license: itemValue === "all" ? [] : [itemValue]
|
||||
} );
|
||||
}}
|
||||
items={photoLicenses}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={unappliedFilters.photo_license.length > 0 ? unappliedFilters.photo_license[0] : "all"}
|
||||
/>
|
||||
<TranslatedText text="Description-Tags" />
|
||||
<InputField
|
||||
handleTextChange={( q ) => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
q
|
||||
} );
|
||||
}}
|
||||
placeholder={t( "Search-for-description-tags-text" )}
|
||||
text={unappliedFilters.q}
|
||||
type="none"
|
||||
/>
|
||||
<View style={viewStyles.bottomPadding} />
|
||||
</ScrollNoFooter>
|
||||
<ExploreFooter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
39
src/components/Explore/ExploreFooter.js
Normal file
39
src/components/Explore/ExploreFooter.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import { viewStyles } from "../../styles/explore/exploreFilters";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
|
||||
const ExploreFooter = ( ): Node => {
|
||||
const { applyFilters, resetUnappliedFilters } = React.useContext( ExploreContext );
|
||||
const navigation = useNavigation( );
|
||||
|
||||
const applyFiltersAndNavigate = ( ) => {
|
||||
applyFilters( );
|
||||
navigation.goBack( );
|
||||
};
|
||||
|
||||
const clearFiltersAndNavigate = ( ) => {
|
||||
resetUnappliedFilters( );
|
||||
navigation.goBack( );
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={viewStyles.footer}>
|
||||
<HeaderBackButton onPress={clearFiltersAndNavigate} style={viewStyles.element}/>
|
||||
<RoundGreenButton
|
||||
handlePress={applyFiltersAndNavigate}
|
||||
buttonText="Apply Filters"
|
||||
testID="ExploreFilters.applyFilters"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreFooter;
|
||||
49
src/components/Explore/ExploreLanding.js
Normal file
49
src/components/Explore/ExploreLanding.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import type { Node } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import { textStyles, viewStyles } from "../../styles/explore/explore";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
// import DropdownPicker from "./DropdownPicker";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import FiltersIcon from "./FiltersIcon";
|
||||
import TaxonLocationSearch from "./TaxonLocationSearch";
|
||||
|
||||
const Explore = ( ): Node => {
|
||||
const {
|
||||
setLoading,
|
||||
exploreFilters
|
||||
} = useContext( ExploreContext );
|
||||
const navigation = useNavigation( );
|
||||
|
||||
const navToExplore = ( ) => {
|
||||
setLoading( );
|
||||
navigation.navigate( "Explore" );
|
||||
};
|
||||
|
||||
const taxonId = exploreFilters ? exploreFilters.taxon_id : null;
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<TranslatedText text="Explore" />
|
||||
<TranslatedText style={textStyles.explanation} text="Visually-search-iNaturalist-data" />
|
||||
<FiltersIcon />
|
||||
<TaxonLocationSearch />
|
||||
<View style={viewStyles.positionBottom}>
|
||||
<RoundGreenButton
|
||||
buttonText="Explore"
|
||||
handlePress={navToExplore}
|
||||
testID="Explore.fetchObservations"
|
||||
disabled={!taxonId}
|
||||
/>
|
||||
</View>
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Explore;
|
||||
23
src/components/Explore/ResetFiltersButton.js
Normal file
23
src/components/Explore/ResetFiltersButton.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { Pressable } from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/observations/messagesIcon";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
|
||||
const ResetFiltersButton = ( ): React.Node => {
|
||||
const { resetFilters } = React.useContext( ExploreContext );
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={resetFilters}
|
||||
style={viewStyles.messages}
|
||||
>
|
||||
<TranslatedText text="Reset" />
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetFiltersButton;
|
||||
61
src/components/Explore/TaxonLocationSearch.js
Normal file
61
src/components/Explore/TaxonLocationSearch.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import type { Node } from "react";
|
||||
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
|
||||
const TaxonLocationSearch = ( ): Node => {
|
||||
const {
|
||||
exploreFilters,
|
||||
setExploreFilters,
|
||||
taxon,
|
||||
setTaxon,
|
||||
location,
|
||||
setLocation
|
||||
} = useContext( ExploreContext );
|
||||
|
||||
const setTaxonId = ( getValue ) => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
taxon_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const setPlaceId = ( getValue ) => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
place_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const taxonId = exploreFilters ? exploreFilters.taxon_id : null;
|
||||
const placeId = exploreFilters ? exploreFilters.place_id : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TranslatedText text="Taxon" />
|
||||
<DropdownPicker
|
||||
searchQuery={taxon}
|
||||
setSearchQuery={setTaxon}
|
||||
setValue={setTaxonId}
|
||||
sources="taxa"
|
||||
value={taxonId}
|
||||
placeholder="Search-for-a-taxon"
|
||||
/>
|
||||
<TranslatedText text="Location" />
|
||||
<DropdownPicker
|
||||
searchQuery={location}
|
||||
setSearchQuery={setLocation}
|
||||
setValue={setPlaceId}
|
||||
sources="places"
|
||||
value={placeId}
|
||||
placeholder="Search-for-a-location"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaxonLocationSearch;
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Text, View, Image } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import TinderCard from "react-tinder-card";
|
||||
|
||||
@@ -130,9 +130,12 @@ const authenticateUser = async (
|
||||
return false;
|
||||
}
|
||||
|
||||
const userId = userDetails.userId && userDetails.userId.toString( );
|
||||
|
||||
// Save authentication details to secure storage
|
||||
await SInfo.setItem( "username", userDetails.username, {} );
|
||||
await SInfo.setItem( "accessToken", userDetails.accessToken, {} );
|
||||
await SInfo.setItem( "userId", userId, {} );
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -247,11 +250,13 @@ const verifyCredentials = async (
|
||||
}
|
||||
|
||||
const iNatUsername = response.data.login;
|
||||
const iNatID = response.data.id;
|
||||
console.log( "verifyCredentials - logged in username ", iNatUsername );
|
||||
|
||||
return {
|
||||
accessToken: accessToken,
|
||||
username: iNatUsername
|
||||
username: iNatUsername,
|
||||
userId: iNatID
|
||||
};
|
||||
};
|
||||
|
||||
@@ -274,6 +279,15 @@ const getUsername = async (): Promise<string> => {
|
||||
return await RNSInfo.getItem( "username", {} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the logged-in userId
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const getUserId = async (): Promise<string> => {
|
||||
return await RNSInfo.getItem( "userId", {} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Signs out the user
|
||||
*
|
||||
@@ -293,5 +307,6 @@ export {
|
||||
isLoggedIn,
|
||||
getUsername,
|
||||
signOut,
|
||||
getJWTToken
|
||||
getJWTToken,
|
||||
getUserId
|
||||
};
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// @flow strict-local
|
||||
// @flow
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Text, TextInput } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { textStyles } from "../../styles/login/login";
|
||||
import { isLoggedIn, authenticateUser, getUsername, signOut } from "./AuthenticationService";
|
||||
import { isLoggedIn, authenticateUser, getUsername, getUserId, signOut } from "./AuthenticationService";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
|
||||
const Login = (): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const [email, setEmail] = useState( "" );
|
||||
const [password, setPassword] = useState( "" );
|
||||
const [loggedIn, setLoggedIn] = useState( false );
|
||||
@@ -40,8 +42,14 @@ const Login = (): Node => {
|
||||
return;
|
||||
}
|
||||
|
||||
setUsername( await getUsername() );
|
||||
const userLogin = await getUsername( );
|
||||
const userId = await getUserId( );
|
||||
setUsername( userLogin );
|
||||
setLoggedIn( true );
|
||||
navigation.navigate( "my observations", {
|
||||
screen: "ObsList",
|
||||
params: { syncData: true, userLogin, userId }
|
||||
} );
|
||||
};
|
||||
|
||||
const onSignOut = async () => {
|
||||
@@ -49,6 +57,13 @@ const Login = (): Node => {
|
||||
setLoggedIn( false );
|
||||
};
|
||||
|
||||
useEffect( ( ) => {
|
||||
navigation.addListener( "blur", ( ) => {
|
||||
setEmail( "" );
|
||||
setPassword( "" );
|
||||
}, [] );
|
||||
}, [navigation] );
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
{!loggedIn ? (
|
||||
@@ -62,6 +77,7 @@ const Login = (): Node => {
|
||||
value={email}
|
||||
autoComplete="email"
|
||||
testID="Login.email"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<Text style={textStyles.text}>Password</Text>
|
||||
<TextInput
|
||||
|
||||
@@ -35,7 +35,7 @@ const ActivityItem = ( { item, navToTaxonDetails, handlePress }: Props ): React.
|
||||
{item.vision && <Text>vision</Text>}
|
||||
<Text>{item.category}</Text>
|
||||
{item.created_at && <Text>{timeAgo( item.created_at )}</Text>}
|
||||
<FlagDropdown id={item} />
|
||||
<FlagDropdown />
|
||||
</View>
|
||||
{taxon && (
|
||||
<Pressable
|
||||
|
||||
53
src/components/ObsEdit/BottomModal.js
Normal file
53
src/components/ObsEdit/BottomModal.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import { viewStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
|
||||
const BottomModal = ( ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const {
|
||||
observations,
|
||||
setObservations
|
||||
} = useContext( ObsEditContext );
|
||||
|
||||
const deleteObsAndNavigate = ( ) => {
|
||||
setObservations( [] );
|
||||
navigation.goBack( );
|
||||
};
|
||||
|
||||
const saveObsAndNavigate = ( ) => {
|
||||
console.log( "save to realm for later upload" );
|
||||
setObservations( [] );
|
||||
navigation.goBack( );
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={viewStyles.bottomModal}>
|
||||
<Text>cancel creating observations?</Text>
|
||||
<Text>by exiting...</Text>
|
||||
<View style={viewStyles.row}>
|
||||
<View style={viewStyles.saveButton}>
|
||||
<RoundGreenButton
|
||||
buttonText="save"
|
||||
testID="ObsEdit.saveButton"
|
||||
handlePress={saveObsAndNavigate}
|
||||
/>
|
||||
</View>
|
||||
<RoundGreenButton
|
||||
buttonText="DELETE-X-OBSERVATIONS"
|
||||
count={observations.length}
|
||||
handlePress={deleteObsAndNavigate}
|
||||
testID="ObsEdit.exitNavigation"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomModal;
|
||||
@@ -13,6 +13,10 @@ import { viewStyles, textStyles } from "../../styles/obsEdit/cvSuggestions";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import useRemoteObsEditSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import { useLoggedIn } from "../../sharedHooks/useLoggedIn";
|
||||
import { t } from "i18next";
|
||||
// TODO: do we need custom hook useTranslation or can we just use t from "i18next"?
|
||||
// saves some lines of code if we don't need the extra hook
|
||||
|
||||
const CVSuggestions = ( ): Node => {
|
||||
const {
|
||||
@@ -26,8 +30,10 @@ const CVSuggestions = ( ): Node => {
|
||||
const [selectedPhoto, setSelectedPhoto] = useState( 0 );
|
||||
const [q, setQ] = React.useState( "" );
|
||||
const list = useRemoteObsEditSearchResults( q, "taxa" );
|
||||
const isLoggedIn = useLoggedIn( );
|
||||
|
||||
const currentObs = observations[currentObsNumber];
|
||||
const hasPhotos = currentObs.observationPhotos;
|
||||
const suggestions = useCVSuggestions( currentObs, showSeenNearby, selectedPhoto );
|
||||
|
||||
const renderNavButtons = ( updateIdentification, id ) => {
|
||||
@@ -46,11 +52,14 @@ const CVSuggestions = ( ): Node => {
|
||||
};
|
||||
|
||||
const renderSuggestions = ( { item } ) => {
|
||||
const uri = { uri: item.taxon.taxon_photos[0].photo.medium_url };
|
||||
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 = ( ) => {
|
||||
setIdentification( item.taxon );
|
||||
updateTaxaId( item.taxon.id );
|
||||
setIdentification( taxon );
|
||||
updateTaxaId( taxon.id );
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -60,11 +69,11 @@ const CVSuggestions = ( ): Node => {
|
||||
style={viewStyles.imageBackground}
|
||||
/>
|
||||
<View style={viewStyles.obsDetailsColumn}>
|
||||
<Text style={textStyles.text}>{item.taxon.preferred_common_name}</Text>
|
||||
<Text style={textStyles.text}>{item.taxon.name}</Text>
|
||||
<Text style={textStyles.text}>{taxon.preferred_common_name}</Text>
|
||||
<Text style={textStyles.text}>{taxon.name}</Text>
|
||||
{showSeenNearby && <Text style={textStyles.greenText}>seen nearby</Text>}
|
||||
</View>
|
||||
{renderNavButtons( updateIdentification, item.taxon.id )}
|
||||
{renderNavButtons( updateIdentification, taxon.id )}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -97,11 +106,19 @@ const CVSuggestions = ( ): Node => {
|
||||
|
||||
const toggleSeenNearby = ( ) => setShowSeenNearby( !showSeenNearby );
|
||||
|
||||
const emptySuggestionsList = ( ) => {
|
||||
if ( !isLoggedIn ) {
|
||||
return <Text style={textStyles.explainerText}>you must be logged in to see computer vision suggestions</Text>;
|
||||
} else {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
};
|
||||
|
||||
const displaySuggestions = ( ) => (
|
||||
<FlatList
|
||||
data={suggestions}
|
||||
renderItem={renderSuggestions}
|
||||
ListEmptyComponent={( ) => <ActivityIndicator />}
|
||||
ListEmptyComponent={hasPhotos && emptySuggestionsList}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -115,15 +132,16 @@ const CVSuggestions = ( ): Node => {
|
||||
return (
|
||||
<ViewNoFooter>
|
||||
<View>
|
||||
<EvidenceList
|
||||
currentObs={currentObs}
|
||||
setSelectedPhoto={setSelectedPhoto}
|
||||
selectedPhoto={selectedPhoto}
|
||||
/>
|
||||
<Text style={textStyles.explainerText}>Select the identification you want to add to this observation...</Text>
|
||||
{hasPhotos && (
|
||||
<EvidenceList
|
||||
currentObs={currentObs}
|
||||
setSelectedPhoto={setSelectedPhoto}
|
||||
selectedPhoto={selectedPhoto}
|
||||
/>
|
||||
)}
|
||||
<InputField
|
||||
handleTextChange={setQ}
|
||||
placeholder="search for taxa"
|
||||
placeholder={t( "Tap-to-search-for-taxa" )}
|
||||
text={q}
|
||||
type="none"
|
||||
/>
|
||||
|
||||
48
src/components/ObsEdit/DatePicker.js
Normal file
48
src/components/ObsEdit/DatePicker.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Text, Pressable } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { textStyles } from "../../styles/obsEdit/obsEdit";
|
||||
|
||||
import DateTimePicker from "../SharedComponents/DateTimePicker";
|
||||
|
||||
type Props = {
|
||||
displayDate: ?string,
|
||||
handleDatePicked: ( Date ) => void
|
||||
}
|
||||
|
||||
const DatePicker = ( { displayDate, handleDatePicked }: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const [showModal, setShowModal] = useState( false );
|
||||
|
||||
const openModal = () => setShowModal( true );
|
||||
const closeModal = () => setShowModal( false );
|
||||
|
||||
const handlePicked = ( value ) => {
|
||||
handleDatePicked( value );
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DateTimePicker
|
||||
datetime
|
||||
isDateTimePickerVisible={showModal}
|
||||
onDatePicked={handlePicked}
|
||||
toggleDateTimePicker={closeModal}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={openModal}
|
||||
>
|
||||
<Text style={textStyles.text} testID="ObsEdit.time">
|
||||
{displayDate || t( "Add-Date-Time" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
52
src/components/ObsEdit/Notes.js
Normal file
52
src/components/ObsEdit/Notes.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { TextInput, Keyboard } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { textStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import { colors } from "../../styles/global";
|
||||
|
||||
type Props = {
|
||||
addNotes: Function
|
||||
}
|
||||
|
||||
const Notes = ( { addNotes }: Props ): Node => {
|
||||
const [keyboardOffset, setKeyboardOffset] = useState( 0 );
|
||||
|
||||
useEffect( ( ) => {
|
||||
const showSubscription = Keyboard.addListener( "keyboardDidShow", ( e ) => {
|
||||
setKeyboardOffset( e.endCoordinates.height );
|
||||
} );
|
||||
const hideSubscription = Keyboard.addListener( "keyboardDidHide", ( e ) => {
|
||||
setKeyboardOffset( 0 );
|
||||
} );
|
||||
|
||||
return ( ) => {
|
||||
showSubscription.remove( );
|
||||
hideSubscription.remove( );
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const offset = {
|
||||
bottom: keyboardOffset,
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
width: "90%",
|
||||
backgroundColor: colors.white
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
keyboardType="default"
|
||||
multiline
|
||||
onChangeText={addNotes}
|
||||
placeholder={t( "Add-optional-notes" )}
|
||||
style={[textStyles.notes, keyboardOffset > 0 && offset]}
|
||||
testID="ObsEdit.notes"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notes;
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useCallback, useContext } from "react";
|
||||
import { Text, TextInput, Pressable, FlatList, View, Modal, Platform, Alert } from "react-native";
|
||||
import { Text, Pressable, FlatList, View, Modal, Platform, Alert } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import RNPickerSelect from "react-native-picker-select";
|
||||
import type { Node } from "react";
|
||||
@@ -21,6 +21,11 @@ import { ObsEditContext } from "../../providers/contexts";
|
||||
import useLocationName from "../../sharedHooks/useLocationName";
|
||||
import EvidenceList from "./EvidenceList";
|
||||
import resizeImageForUpload from "./helpers/resizeImage";
|
||||
import { useLoggedIn } from "../../sharedHooks/useLoggedIn";
|
||||
import DatePicker from "./DatePicker";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import Notes from "./Notes";
|
||||
import BottomModal from "./BottomModal";
|
||||
|
||||
const ObsEdit = ( ): Node => {
|
||||
const {
|
||||
@@ -34,23 +39,27 @@ const ObsEdit = ( ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const { t } = useTranslation( );
|
||||
const [showModal, setModal] = useState( false );
|
||||
const [showBottomModal, setBottomModal] = useState( false );
|
||||
const [source, setSource] = useState( null );
|
||||
const isLoggedIn = useLoggedIn( );
|
||||
|
||||
const openModal = useCallback( ( ) => setModal( true ), [] );
|
||||
const closeModal = useCallback( ( ) => setModal( false ), [] );
|
||||
const openBottomModal = useCallback( ( ) => setBottomModal( true ), [] );
|
||||
const closeBottomModal = useCallback( ( ) => setBottomModal( false ), [] );
|
||||
|
||||
const [showLocationPicker, setShowLocationPicker] = useState( false );
|
||||
|
||||
const geoprivacyOptions = [{
|
||||
label: "open",
|
||||
label: t( "Open" ),
|
||||
value: "open"
|
||||
},
|
||||
{
|
||||
label: "obscured",
|
||||
label: t( "Obscured" ),
|
||||
value: "obscured"
|
||||
},
|
||||
{
|
||||
label: "private",
|
||||
label: t( "Private" ),
|
||||
value: "private"
|
||||
}];
|
||||
|
||||
@@ -70,6 +79,7 @@ const ObsEdit = ( ): Node => {
|
||||
const updateGeoprivacyStatus = value => updateObservationKey( "geoprivacy", value );
|
||||
const updateCaptiveStatus = value => updateObservationKey( "captive_flag", value );
|
||||
const updateTaxaId = taxaId => updateObservationKey( "taxon_id", taxaId );
|
||||
const updateObservedOn = value => updateObservationKey( "observed_on_string", value );
|
||||
|
||||
const updateProjectIds = projectId => {
|
||||
const updatedObs = observations.map( ( obs, index ) => {
|
||||
@@ -111,26 +121,35 @@ const ObsEdit = ( ): Node => {
|
||||
const renderArrowNavigation = ( ) => {
|
||||
if ( observations.length === 0 ) { return; }
|
||||
|
||||
const handleBackButtonPress = ( ) => {
|
||||
openBottomModal( );
|
||||
// show modal to dissuade user from going back
|
||||
// navigation.goBack( );
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={viewStyles.row}>
|
||||
<HeaderBackButton onPress={( ) => navigation.goBack( )} />
|
||||
<View style={viewStyles.row}>
|
||||
{currentObsNumber !== 0 && (
|
||||
<Pressable
|
||||
onPress={showPrevObservation}
|
||||
>
|
||||
<Text>previous obs</Text>
|
||||
</Pressable>
|
||||
<HeaderBackButton onPress={handleBackButtonPress} />
|
||||
{observations.length === 1
|
||||
? <TranslatedText text="New-Observation" /> : (
|
||||
<View style={viewStyles.row}>
|
||||
{currentObsNumber !== 0 && (
|
||||
<Pressable
|
||||
onPress={showPrevObservation}
|
||||
>
|
||||
<Text>previous obs</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
<Text>{`${currentObsNumber + 1} of ${observations.length}`}</Text>
|
||||
{( currentObsNumber !== observations.length - 1 ) && (
|
||||
<Pressable
|
||||
onPress={showNextObservation}
|
||||
>
|
||||
<Text>next obs</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<Text>{`${currentObsNumber + 1} of ${observations.length}`}</Text>
|
||||
{( currentObsNumber !== observations.length - 1 ) && (
|
||||
<Pressable
|
||||
onPress={showNextObservation}
|
||||
>
|
||||
<Text>next obs</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
<View />
|
||||
</View>
|
||||
);
|
||||
@@ -270,6 +289,12 @@ const ObsEdit = ( ): Node => {
|
||||
setObservations( updatedObs );
|
||||
};
|
||||
|
||||
const handleDatePicked = ( selectedDate ) => {
|
||||
if ( selectedDate ) {
|
||||
updateObservedOn( selectedDate );
|
||||
}
|
||||
};
|
||||
|
||||
const renderLocationPickerModal = ( ) => (
|
||||
<Modal visible={showLocationPicker}>
|
||||
<LocationPicker
|
||||
@@ -281,7 +306,8 @@ const ObsEdit = ( ): Node => {
|
||||
|
||||
if ( !currentObs ) { return null; }
|
||||
|
||||
const displayDate = currentObs.observed_on_string ? `Date & time: ${currentObs.observed_on_string}` : null;
|
||||
// TODO: make sure observed_on_string uses same time format with all types of evidence (camera, gallery, etc)
|
||||
const displayDate = currentObs.observed_on_string ? `${currentObs.observed_on_string}` : null;
|
||||
|
||||
const displayLocation = ( ) => {
|
||||
let location = "";
|
||||
@@ -345,8 +371,16 @@ const ObsEdit = ( ): Node => {
|
||||
|
||||
return (
|
||||
<ScrollNoFooter>
|
||||
<CustomModal
|
||||
showModal={showBottomModal}
|
||||
closeModal={closeBottomModal}
|
||||
modal={(
|
||||
<BottomModal />
|
||||
)}
|
||||
style={viewStyles.noMargin}
|
||||
/>
|
||||
{renderLocationPickerModal( )}
|
||||
<CustomModal
|
||||
<CustomModal
|
||||
showModal={showModal}
|
||||
closeModal={closeModal}
|
||||
modal={(
|
||||
@@ -358,60 +392,69 @@ const ObsEdit = ( ): Node => {
|
||||
)}
|
||||
/>
|
||||
{renderArrowNavigation( )}
|
||||
<Text style={textStyles.headerText}>{ t( "Evidence" )}</Text>
|
||||
<TranslatedText style={textStyles.headerText} text="Evidence" />
|
||||
{/* TODO: allow user to tap into bigger version of photo (crop screen) */}
|
||||
<EvidenceList currentObs={currentObs} showCameraOptions />
|
||||
<Pressable
|
||||
onPress={openLocationPicker}
|
||||
>
|
||||
<Text style={textStyles.text}>
|
||||
{placeGuess}
|
||||
{placeGuess || t( "Add-Location" )}
|
||||
</Text>
|
||||
<Text style={textStyles.text}>
|
||||
{displayLocation( )}
|
||||
{displayLocation( ) || t( "No-Location" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Text style={textStyles.text} testID="ObsEdit.time">{displayDate}</Text>
|
||||
<Text style={textStyles.headerText}>{ t( "Identification" )}</Text>
|
||||
<DatePicker displayDate={displayDate} handleDatePicked={handleDatePicked} />
|
||||
<TranslatedText style={textStyles.headerText} text="Identification" />
|
||||
{displayIdentification( )}
|
||||
<FlatList
|
||||
data={Object.keys( iconicTaxaIds )}
|
||||
horizontal
|
||||
renderItem={renderIconicTaxaButton}
|
||||
/>
|
||||
<Text style={textStyles.headerText}>{ t( "Other-Data" )}</Text>
|
||||
<Text style={textStyles.text}>geoprivacy</Text>
|
||||
<RNPickerSelect
|
||||
onValueChange={updateGeoprivacyStatus}
|
||||
items={geoprivacyOptions}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={currentObs.geoprivacy}
|
||||
/>
|
||||
<Text style={textStyles.text}>is the organism wild?</Text>
|
||||
<RNPickerSelect
|
||||
onValueChange={updateCaptiveStatus}
|
||||
items={captiveOptions}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={currentObs.captive_flag}
|
||||
/>
|
||||
<TranslatedText style={textStyles.headerText} text="Other-Data" />
|
||||
<View style={viewStyles.row}>
|
||||
<TranslatedText style={textStyles.text} text="Geoprivacy" />
|
||||
<RNPickerSelect
|
||||
onValueChange={updateGeoprivacyStatus}
|
||||
items={geoprivacyOptions}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={currentObs.geoprivacy}
|
||||
/>
|
||||
|
||||
</View>
|
||||
<View style={viewStyles.row}>
|
||||
<Text style={textStyles.text}>is the organism wild?</Text>
|
||||
<RNPickerSelect
|
||||
onValueChange={updateCaptiveStatus}
|
||||
items={captiveOptions}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={currentObs.captive_flag}
|
||||
/>
|
||||
</View>
|
||||
<Notes addNotes={addNotes} />
|
||||
<Pressable onPress={searchForProjects}>
|
||||
<Text style={textStyles.text}>tap to add projects</Text>
|
||||
<TranslatedText style={textStyles.text} text="Add-to-projects" />
|
||||
</Pressable>
|
||||
<TextInput
|
||||
keyboardType="default"
|
||||
multiline
|
||||
onChangeText={addNotes}
|
||||
placeholder="add optional notes"
|
||||
style={textStyles.notes}
|
||||
testID="ObsEdit.notes"
|
||||
/>
|
||||
<RoundGreenButton
|
||||
buttonText="upload obs"
|
||||
testID="ObsEdit.uploadButton"
|
||||
handlePress={uploadObservation}
|
||||
/>
|
||||
{!isLoggedIn && <Text style={textStyles.text}>you must be logged in to upload observations</Text>}
|
||||
<View style={viewStyles.row}>
|
||||
<View style={viewStyles.saveButton}>
|
||||
<RoundGreenButton
|
||||
buttonText="save"
|
||||
testID="ObsEdit.saveButton"
|
||||
handlePress={( ) => console.log( "save to realm for later upload" )}
|
||||
/>
|
||||
</View>
|
||||
<RoundGreenButton
|
||||
buttonText="UPLOAD-OBSERVATION"
|
||||
testID="ObsEdit.uploadButton"
|
||||
handlePress={uploadObservation}
|
||||
disabled={!isLoggedIn}
|
||||
/>
|
||||
</View>
|
||||
</ScrollNoFooter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ const useCVSuggestions = ( currentObs: Object, showSeenNearby: boolean, selected
|
||||
const [suggestions, setSuggestions] = useState( [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( !currentObs ) { return; }
|
||||
if ( !currentObs || !currentObs.observationPhotos ) { return; }
|
||||
const uri = currentObs.observationPhotos && currentObs.observationPhotos[selectedPhoto].uri;
|
||||
const latitude = currentObs.latitude;
|
||||
const longitude = currentObs.longitude;
|
||||
@@ -45,7 +45,7 @@ const useCVSuggestions = ( currentObs: Object, showSeenNearby: boolean, selected
|
||||
name: "photo.jpeg",
|
||||
type: "image/jpeg"
|
||||
} ),
|
||||
fields: JSON.stringify( FIELDS )
|
||||
fields: FIELDS
|
||||
};
|
||||
|
||||
if ( showSeenNearby ) {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { Text, Pressable } from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/observations/messagesIcon";
|
||||
|
||||
const MessagesIcon = ( ): React.Node => (
|
||||
<Pressable onPress={( ) => console.log( "navigate to messages" )} style={viewStyles.messages}>
|
||||
<Text>messages</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default MessagesIcon;
|
||||
@@ -1,18 +1,34 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import type { Node } from "react";
|
||||
import { Pressable, Text } from "react-native";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import { ObservationContext } from "../../providers/contexts";
|
||||
import ObservationViews from "../SharedComponents/ObservationViews/ObservationViews";
|
||||
import UserCard from "./UserCard";
|
||||
import { useCurrentUser } from "./hooks/useCurrentUser";
|
||||
|
||||
const ObsList = ( ): Node => {
|
||||
const { observationList, loading, syncObservations } = useContext( ObservationContext );
|
||||
const { params } = useRoute( );
|
||||
const { observationList, loading, syncObservations, fetchNextObservations } = useContext( ObservationContext );
|
||||
|
||||
const id = params && params.userId;
|
||||
|
||||
useEffect( ( ) => {
|
||||
// start fetching data immediately after successful login
|
||||
if ( params && params.syncData ) {
|
||||
syncObservations( params.userLogin );
|
||||
}
|
||||
}, [params, syncObservations] );
|
||||
|
||||
const userId = useCurrentUser( );
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<UserCard userId={userId || id} />
|
||||
<Pressable onPress={syncObservations}>
|
||||
<Text>sync</Text>
|
||||
</Pressable>
|
||||
@@ -20,6 +36,7 @@ const ObsList = ( ): Node => {
|
||||
loading={loading}
|
||||
observationList={observationList}
|
||||
testID="ObsList.myObservations"
|
||||
handleEndReached={fetchNextObservations}
|
||||
/>
|
||||
</ViewWithFooter>
|
||||
);
|
||||
|
||||
42
src/components/Observations/UserCard.js
Normal file
42
src/components/Observations/UserCard.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Text, View, Pressable } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
import { useUser } from "../UserProfile/hooks/useUser";
|
||||
import User from "../../models/User";
|
||||
import { viewStyles } from "../../styles/observations/userCard";
|
||||
|
||||
type Props = {
|
||||
userId: number
|
||||
}
|
||||
|
||||
const UserCard = ( { userId }: Props ): Node => {
|
||||
// TODO: this currently doesn't show up on initial login
|
||||
// because user id can't be fetched
|
||||
const navigation = useNavigation( );
|
||||
const { user } = useUser( userId );
|
||||
const navToUserProfile = ( ) => navigation.navigate( "UserProfile", { userId } );
|
||||
|
||||
if ( !user ) { return null; }
|
||||
|
||||
return (
|
||||
<View style={viewStyles.userCard}>
|
||||
<UserIcon uri={User.uri( user )} large />
|
||||
<View style={viewStyles.userDetails}>
|
||||
<Text>{User.userHandle( user )}</Text>
|
||||
<Text>{`${user.observations_count} Observations`}</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={navToUserProfile}
|
||||
>
|
||||
<Text>edit profile</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserCard;
|
||||
40
src/components/Observations/hooks/useCurrentUser.js
Normal file
40
src/components/Observations/hooks/useCurrentUser.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
|
||||
import { getJWTToken } from "../../../components/LoginSignUp/AuthenticationService";
|
||||
|
||||
const useCurrentUser = ( ): Object => {
|
||||
const [currentUser, setCurrentUser] = useState( null );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchUserProfile = async ( ) => {
|
||||
try {
|
||||
const apiToken = await getJWTToken( false );
|
||||
const options = {
|
||||
api_token: apiToken
|
||||
};
|
||||
const response = await inatjs.users.me( options );
|
||||
const results = response.results;
|
||||
if ( !isCurrent ) { return; }
|
||||
setCurrentUser( results[0].id );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch current user:", e.message );
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserProfile( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [] );
|
||||
|
||||
return currentUser;
|
||||
};
|
||||
|
||||
export {
|
||||
useCurrentUser
|
||||
};
|
||||
@@ -105,7 +105,6 @@ const GroupPhotos = ( ): Node => {
|
||||
const extractKey = ( item, index ) => `${item.observationPhotos[0].uri}${index}`;
|
||||
|
||||
const groupedPhotos = obsToEdit.observations;
|
||||
const photoSelected = selectedObservations.length > 0;
|
||||
|
||||
const flattenAndOrderSelectedPhotos = ( ) => {
|
||||
// combine selected observations into a single array
|
||||
@@ -147,7 +146,17 @@ const GroupPhotos = ( ): Node => {
|
||||
};
|
||||
|
||||
const separatePhotos = ( ) => {
|
||||
if ( selectedObservations.length < 2 ) { return; }
|
||||
let maxCombinedPhotos = 0;
|
||||
|
||||
selectedObservations.forEach( obs => {
|
||||
const numPhotos = obs.observationPhotos.length;
|
||||
if ( numPhotos > maxCombinedPhotos ) {
|
||||
maxCombinedPhotos = numPhotos;
|
||||
}
|
||||
} );
|
||||
|
||||
// make sure at least one set of combined photos is selected
|
||||
if ( maxCombinedPhotos < 2 ) { return; }
|
||||
|
||||
let separatedPhotos = [];
|
||||
const orderedPhotos = flattenAndOrderSelectedPhotos( );
|
||||
@@ -217,8 +226,6 @@ const GroupPhotos = ( ): Node => {
|
||||
<GroupPhotosHeader
|
||||
photos={observations.length}
|
||||
observations={groupedPhotos.length}
|
||||
isSelected={photoSelected}
|
||||
clearSelection={clearSelection}
|
||||
/>
|
||||
<FlatList
|
||||
contentContainerStyle={viewStyles.centerImages}
|
||||
@@ -235,6 +242,8 @@ const GroupPhotos = ( ): Node => {
|
||||
separatePhotos={separatePhotos}
|
||||
removePhotos={removePhotos}
|
||||
navToObsEdit={navToObsEdit}
|
||||
clearSelection={clearSelection}
|
||||
selectedObservations={selectedObservations}
|
||||
/>
|
||||
</ViewNoFooter>
|
||||
);
|
||||
|
||||
@@ -1,40 +1,82 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { View, Pressable, Text } from "react-native";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { View, Pressable } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { viewStyles } from "../../styles/photoLibrary/photoGalleryHeader";
|
||||
import { viewStyles, textStyles } from "../../styles/photoLibrary/photoGalleryHeader";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import Modal from "../SharedComponents/Modal";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
|
||||
type Props = {
|
||||
combinePhotos: Function,
|
||||
separatePhotos: Function,
|
||||
removePhotos: Function,
|
||||
navToObsEdit: Function
|
||||
navToObsEdit: Function,
|
||||
clearSelection: Function,
|
||||
selectedObservations: Array<Object>
|
||||
}
|
||||
|
||||
const GroupPhotosFooter = ( {
|
||||
combinePhotos,
|
||||
separatePhotos,
|
||||
removePhotos,
|
||||
navToObsEdit
|
||||
}: Props ): Node => (
|
||||
<View style={viewStyles.footer}>
|
||||
<View>
|
||||
<Pressable onPress={combinePhotos}>
|
||||
<Text>Combine photos</Text>
|
||||
navToObsEdit,
|
||||
clearSelection,
|
||||
selectedObservations
|
||||
}: Props ): Node => {
|
||||
const [showModal, setModal] = useState( false );
|
||||
|
||||
const openModal = useCallback( ( ) => setModal( true ), [] );
|
||||
const closeModal = useCallback( ( ) => setModal( false ), [] );
|
||||
|
||||
const multipleObsSelected = selectedObservations.length > 1;
|
||||
const isSelected = selectedObservations.length > 0;
|
||||
|
||||
const combineStyle = [textStyles.selections, !multipleObsSelected && textStyles.disabled];
|
||||
const selectionStyle = [textStyles.selections, !isSelected && textStyles.disabled];
|
||||
|
||||
const selectionModal = ( ) => (
|
||||
<View style={viewStyles.selectionModal}>
|
||||
<Pressable onPress={combinePhotos} disabled={!multipleObsSelected}>
|
||||
<TranslatedText style={combineStyle} text="Combine-Photos" />
|
||||
</Pressable>
|
||||
<Pressable onPress={separatePhotos}>
|
||||
<Text>Separate photos</Text>
|
||||
<Pressable onPress={separatePhotos} disabled={!isSelected}>
|
||||
<TranslatedText style={selectionStyle} text="Separate-Photos" />
|
||||
</Pressable>
|
||||
<Pressable onPress={removePhotos}>
|
||||
<Text>Remove photos</Text>
|
||||
<Pressable onPress={removePhotos} disabled={!isSelected}>
|
||||
<TranslatedText style={selectionStyle} text="Remove-Photos" />
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable onPress={navToObsEdit}>
|
||||
<Text>next</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={viewStyles.footer}>
|
||||
<Modal
|
||||
showModal={showModal}
|
||||
closeModal={closeModal}
|
||||
modal={selectionModal( )}
|
||||
/>
|
||||
<Pressable onPress={openModal}>
|
||||
<TranslatedText text="Select" />
|
||||
</Pressable>
|
||||
{isSelected && (
|
||||
<Pressable
|
||||
onPress={clearSelection}
|
||||
>
|
||||
<TranslatedText style={textStyles.header} text="Cancel" />
|
||||
</Pressable>
|
||||
)}
|
||||
<View style={viewStyles.nextButton}>
|
||||
<RoundGreenButton
|
||||
buttonText="Next"
|
||||
handlePress={navToObsEdit}
|
||||
testID="GroupPhotos.next"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupPhotosFooter;
|
||||
|
||||
@@ -1,42 +1,34 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { View, Pressable, Text } from "react-native";
|
||||
import { View, Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/photoLibrary/photoGalleryHeader";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
|
||||
type Props = {
|
||||
photos: number,
|
||||
observations: number,
|
||||
isSelected: boolean,
|
||||
clearSelection: Function
|
||||
observations: number
|
||||
}
|
||||
|
||||
const GroupPhotosHeader = ( { photos, observations, isSelected, clearSelection }: Props ): Node => {
|
||||
const GroupPhotosHeader = ( { photos, observations }: Props ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const navBack = ( ) => navigation.goBack( );
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={viewStyles.header}>
|
||||
<Pressable
|
||||
onPress={navBack}
|
||||
>
|
||||
<Text>back button</Text>
|
||||
</Pressable>
|
||||
<Text style={textStyles.header}>Group Photos</Text>
|
||||
{isSelected && (
|
||||
<Pressable
|
||||
onPress={clearSelection}
|
||||
>
|
||||
<Text style={textStyles.header}>cancel</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
<HeaderBackButton onPress={navBack} />
|
||||
<TranslatedText style={textStyles.header} text="Group-Photos" />
|
||||
</View>
|
||||
<Text>{`${photos} photos, ${observations} observations`}</Text>
|
||||
<Text style={textStyles.header}>{t( "X-photos-X-observations", { photoCount: photos, observationCount: observations } )}</Text>
|
||||
<TranslatedText style={textStyles.text} text="Combine-photos-onboarding" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import { Pressable, Image, FlatList, ActivityIndicator, View } from "react-native";
|
||||
import { Pressable, Image, FlatList, ActivityIndicator, View, Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
@@ -25,7 +25,9 @@ const PhotoGallery = ( ): Node => {
|
||||
photoOptions,
|
||||
setPhotoOptions,
|
||||
selectedPhotos,
|
||||
setSelectedPhotos
|
||||
setSelectedPhotos,
|
||||
fetchingPhotos,
|
||||
totalSelected
|
||||
} = useContext( PhotoGalleryContext );
|
||||
|
||||
const navigation = useNavigation( );
|
||||
@@ -111,7 +113,8 @@ const PhotoGallery = ( ): Node => {
|
||||
return (
|
||||
<View style={viewStyles.createObsButton}>
|
||||
<RoundGreenButton
|
||||
buttonText="create observations"
|
||||
buttonText="Upload-X-photos"
|
||||
count={totalSelected}
|
||||
handlePress={navToGroupPhotos}
|
||||
testID="PhotoGallery.createObsButton"
|
||||
/>
|
||||
@@ -121,6 +124,14 @@ const PhotoGallery = ( ): Node => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const renderEmptyList = ( ) => {
|
||||
if ( fetchingPhotos ) {
|
||||
return <ActivityIndicator />;
|
||||
} else {
|
||||
return <Text>no photos found. if this is your first time opening the app and giving permissions, try restarting the app.</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewNoFooter>
|
||||
<PhotoGalleryHeader updateAlbum={updateAlbum} />
|
||||
@@ -133,7 +144,7 @@ const PhotoGallery = ( ): Node => {
|
||||
renderItem={renderImage}
|
||||
onEndReached={fetchMorePhotos}
|
||||
testID="PhotoGallery.list"
|
||||
ListEmptyComponent={( ) => <ActivityIndicator />}
|
||||
ListEmptyComponent={renderEmptyList( )}
|
||||
/>
|
||||
{renderFooter( )}
|
||||
</ViewNoFooter>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { View, Pressable, Text } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import RNPickerSelect from "react-native-picker-select";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
|
||||
import usePhotoAlbums from "./hooks/usePhotoAlbums";
|
||||
import { viewStyles } from "../../styles/photoLibrary/photoGalleryHeader";
|
||||
@@ -30,11 +31,7 @@ const PhotoGalleryHeader = ( { updateAlbum }: Props ): Node => {
|
||||
|
||||
return (
|
||||
<View style={viewStyles.header}>
|
||||
<Pressable
|
||||
onPress={navBack}
|
||||
>
|
||||
<Text>back button</Text>
|
||||
</Pressable>
|
||||
<HeaderBackButton onPress={navBack} />
|
||||
<RNPickerSelect
|
||||
hideIcon
|
||||
items={albums}
|
||||
|
||||
@@ -16,7 +16,7 @@ const initialStatus = {
|
||||
fetchingPhotos: false
|
||||
};
|
||||
|
||||
const usePhotos = ( options: Object, isScrolling: boolean ): Array<Object> => {
|
||||
const usePhotos = ( options: Object, isScrolling: boolean ): Object => {
|
||||
const [photoFetchStatus, setPhotoFetchStatus] = useState( initialStatus );
|
||||
|
||||
const fetchPhotos = useCallback( async ( ) => {
|
||||
@@ -107,7 +107,7 @@ const usePhotos = ( options: Object, isScrolling: boolean ): Array<Object> => {
|
||||
}
|
||||
}, [photoFetchStatus, options] );
|
||||
|
||||
return photoFetchStatus.photos;
|
||||
return photoFetchStatus;
|
||||
};
|
||||
|
||||
export default usePhotos;
|
||||
|
||||
@@ -18,6 +18,7 @@ const ProjectDetails = ( ): React.Node => {
|
||||
<ViewWithFooter>
|
||||
<ImageBackground
|
||||
source={{ uri: project.header_image_url }}
|
||||
// $FlowFixMe
|
||||
style={imageStyles.headerImage}
|
||||
testID="ProjectDetails.headerImage"
|
||||
>
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Pressable, Text } from "react-native";
|
||||
import { Pressable } from "react-native";
|
||||
|
||||
import { viewStyles, textStyles } from "../../../styles/sharedComponents/buttons/roundGreenButton";
|
||||
import TranslatedText from "../TranslatedText";
|
||||
|
||||
type Props = {
|
||||
buttonText: string,
|
||||
handlePress: any,
|
||||
testID: string,
|
||||
disabled?: boolean
|
||||
disabled?: boolean,
|
||||
count?: number
|
||||
}
|
||||
|
||||
const RoundGreenButton = ( { buttonText, handlePress, testID, disabled }: Props ): React.Node => (
|
||||
<Pressable style={viewStyles.greenButton} onPress={handlePress} testID={testID} disabled={disabled}>
|
||||
<Text style={textStyles.greenButtonText}>
|
||||
{buttonText}
|
||||
</Text>
|
||||
const RoundGreenButton = ( { buttonText, handlePress, testID, disabled, count }: Props ): React.Node => (
|
||||
<Pressable
|
||||
style={[viewStyles.greenButton, disabled && viewStyles.disabled]}
|
||||
onPress={handlePress}
|
||||
testID={testID}
|
||||
disabled={disabled}
|
||||
>
|
||||
<TranslatedText
|
||||
style={textStyles.greenButtonText}
|
||||
text={buttonText}
|
||||
count={count}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
|
||||
42
src/components/SharedComponents/DateTimePicker.js
Normal file
42
src/components/SharedComponents/DateTimePicker.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import DateTimePicker from "react-native-modal-datetime-picker";
|
||||
import { Appearance } from "react-native";
|
||||
|
||||
type Props = {
|
||||
toggleDateTimePicker: Function,
|
||||
onDatePicked: Function,
|
||||
isDateTimePickerVisible: boolean,
|
||||
datetime?: boolean
|
||||
};
|
||||
|
||||
// using component from Seek: https://github.com/inaturalist/SeekReactNative/blob/64ae3df185fffe751aff40ab17e3ff2dd8a74e42/components/UIComponents/DateTimePicker.js
|
||||
|
||||
const DatePicker = ( {
|
||||
datetime,
|
||||
isDateTimePickerVisible,
|
||||
onDatePicked,
|
||||
toggleDateTimePicker
|
||||
}: Props ): React.Node => {
|
||||
const colorScheme = Appearance.getColorScheme( );
|
||||
|
||||
return (
|
||||
<DateTimePicker
|
||||
display="spinner"
|
||||
customHeaderIOS={() => <></>}
|
||||
isDarkModeEnabled={colorScheme === "dark"}
|
||||
isVisible={isDateTimePickerVisible}
|
||||
maximumDate={new Date()}
|
||||
mode={datetime ? "datetime" : "date"}
|
||||
onCancel={toggleDateTimePicker}
|
||||
onConfirm={onDatePicked}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
DatePicker.defaultProps = {
|
||||
datetime: false
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
@@ -7,13 +7,14 @@ import RNModal from "react-native-modal";
|
||||
type Props = {
|
||||
showModal: boolean,
|
||||
closeModal: Function,
|
||||
modal: any
|
||||
modal: any,
|
||||
style?: Object
|
||||
}
|
||||
|
||||
// accessibility might not work on Android because of backdrop
|
||||
// https://github.com/react-native-modal/react-native-modal/issues/525
|
||||
|
||||
const Modal = ( { showModal, closeModal, modal }: Props ): React.Node => (
|
||||
const Modal = ( { showModal, closeModal, modal, style }: Props ): React.Node => (
|
||||
<RNModal
|
||||
isVisible={showModal}
|
||||
onBackdropPress={closeModal}
|
||||
@@ -21,6 +22,7 @@ const Modal = ( { showModal, closeModal, modal }: Props ): React.Node => (
|
||||
swipeDirection="down"
|
||||
useNativeDriverForBackdrop
|
||||
useNativeDriver
|
||||
style={style}
|
||||
>
|
||||
{modal}
|
||||
</RNModal>
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
// @flow strict-local
|
||||
|
||||
import React from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { Text, View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { viewStyles, textStyles } from "../../../styles/sharedComponents/observationViews/obsCard";
|
||||
|
||||
const EmptyList = ( ): Node => {
|
||||
const handlePress = ( ) => console.log( "navigate to learn more" );
|
||||
// const handlePress = ( ) => console.log( "navigate to learn more" );
|
||||
|
||||
return (
|
||||
<View style={viewStyles.center}>
|
||||
<Text style={textStyles.text} testID="ObsList.emptyList">welcome to inaturalist!</Text>
|
||||
<Text style={textStyles.text}>make an obs of an organism, and iNat's AI...</Text>
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
style={viewStyles.row}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={textStyles.text}>learn more</Text>
|
||||
</Pressable>
|
||||
<Text style={textStyles.text}>make sure you're logged in to fetch observations</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Pressable, Text, Image } from "react-native";
|
||||
import { Pressable, Image, Text, View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import Observation from "../../../models/Observation";
|
||||
|
||||
import { textStyles, imageStyles, viewStyles } from "../../../styles/sharedComponents/observationViews/gridItem";
|
||||
import { imageStyles, viewStyles } from "../../../styles/sharedComponents/observationViews/gridItem";
|
||||
import ObsCardDetails from "./ObsCardDetails";
|
||||
import ObsCardStats from "./ObsCardStats";
|
||||
|
||||
type Props = {
|
||||
item: Object,
|
||||
@@ -19,8 +21,9 @@ const GridItem = ( { item, handlePress, uri }: Props ): Node => {
|
||||
// displaying camelcased item keys on ObservationList
|
||||
|
||||
// TODO: add fallback image when there is no uri
|
||||
const imageUri = uri === "project" ? Observation.projectUri( item ) : Observation.uri( item );
|
||||
const commonName = item.taxon && ( item.taxon.preferredCommonName || item.taxon.preferred_common_name );
|
||||
const imageUri = uri === "project" ? Observation.projectUri( item ) : Observation.uri( item, true );
|
||||
|
||||
const totalObsPhotos = item.observationPhotos && item.observationPhotos.length;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -30,12 +33,18 @@ const GridItem = ( { item, handlePress, uri }: Props ): Node => {
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel="Navigate to observation details screen"
|
||||
>
|
||||
{totalObsPhotos > 1 && (
|
||||
<View style={viewStyles.totalObsPhotos}>
|
||||
<Text>{totalObsPhotos}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Image
|
||||
source={imageUri}
|
||||
style={imageStyles.gridImage}
|
||||
testID="ObsList.photo"
|
||||
/>
|
||||
<Text style={textStyles.text}>{commonName}</Text>
|
||||
<ObsCardStats item={item} />
|
||||
<ObsCardDetails item={item} />
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { viewStyles } from "../../../styles/sharedComponents/observationViews/infiniteScroll";
|
||||
|
||||
const InfiniteScrollFooter = ( ): Node => (
|
||||
<View style={viewStyles.infiniteScroll}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
);
|
||||
|
||||
export default InfiniteScrollFooter;
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Pressable, Text, View, Image } from "react-native";
|
||||
import { Pressable, View, Image } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import Observation from "../../../models/Observation";
|
||||
|
||||
import { viewStyles, textStyles } from "../../../styles/sharedComponents/observationViews/obsCard";
|
||||
import { viewStyles } from "../../../styles/sharedComponents/observationViews/obsCard";
|
||||
import ObsCardDetails from "./ObsCardDetails";
|
||||
import ObsCardStats from "./ObsCardStats";
|
||||
|
||||
type Props = {
|
||||
item: Object,
|
||||
@@ -14,15 +16,6 @@ type Props = {
|
||||
|
||||
const ObsCard = ( { item, handlePress }: Props ): Node => {
|
||||
const onPress = ( ) => handlePress( item );
|
||||
// TODO: fix whatever funkiness is preventing realm mapTo from correctly
|
||||
// displaying camelcased item keys on ObservationList
|
||||
|
||||
const commonName = item.taxon && ( item.taxon.preferredCommonName || item.taxon.preferred_common_name );
|
||||
const placeGuess = item.placeGuess || item.place_guess;
|
||||
const timeObserved = item.timeObservedAt || item.time_observed_at;
|
||||
const numOfIds = item.identifications.length || 0;
|
||||
const numOfComments = item.comments.length || 0;
|
||||
const qualityGrade = item.qualityGrade || item.quality_grade;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -39,15 +32,9 @@ const ObsCard = ( { item, handlePress }: Props ): Node => {
|
||||
/>
|
||||
<View style={viewStyles.obsDetailsColumn}>
|
||||
{/* TODO: fill in with actual empty states */}
|
||||
<Text style={textStyles.text}>{commonName || "no common name"}</Text>
|
||||
<Text style={textStyles.text}>{placeGuess || "no place guess"}</Text>
|
||||
<Text style={textStyles.text}>{timeObserved || "no time given"}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={textStyles.text}>{numOfIds || "no ids"}</Text>
|
||||
<Text style={textStyles.text} testID="ObsList.obsCard.commentCount">{numOfComments || "no comments"}</Text>
|
||||
<Text style={textStyles.text}>{qualityGrade || "no quality grade"}</Text>
|
||||
<ObsCardDetails item={item} />
|
||||
</View>
|
||||
<ObsCardStats item={item} type="list" />
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { textStyles } from "../../../styles/sharedComponents/observationViews/obsCard";
|
||||
|
||||
type Props = {
|
||||
item: Object
|
||||
}
|
||||
|
||||
const ObsCardDetails = ( { item }: Props ): Node => {
|
||||
const commonName = item.taxon && ( item.taxon.preferredCommonName || item.taxon.preferred_common_name );
|
||||
const placeGuess = item.placeGuess || item.place_guess;
|
||||
const timeObserved = item.timeObservedAt || item.time_observed_at;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={textStyles.text} numberOfLines={1}>{commonName || "no common name"}</Text>
|
||||
<Text style={textStyles.text} numberOfLines={1}>{placeGuess || "no place guess"}</Text>
|
||||
<Text style={textStyles.text} numberOfLines={1}>{timeObserved || "no time given"}</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObsCardDetails;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { textStyles, viewStyles } from "../../../styles/sharedComponents/observationViews/obsCard";
|
||||
|
||||
type Props = {
|
||||
item: Object,
|
||||
type?: string
|
||||
}
|
||||
|
||||
const ObsCardStats = ( { item, type }: Props ): Node => {
|
||||
const numOfIds = item.identifications.length || 0;
|
||||
const numOfComments = item.comments.length || 0;
|
||||
const qualityGrade = item.qualityGrade || item.quality_grade;
|
||||
|
||||
const renderColumn = ( ) => (
|
||||
<View>
|
||||
<Text style={textStyles.text}>{numOfIds || "no ids"}</Text>
|
||||
<Text style={textStyles.text} testID="ObsList.obsCard.commentCount">{numOfComments || 0}</Text>
|
||||
<Text style={textStyles.text}>{qualityGrade || "no quality grade"}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderRow = ( ) => (
|
||||
<View style={viewStyles.photoStatRow}>
|
||||
<Text style={textStyles.text}>{numOfIds || "no ids"}</Text>
|
||||
<Text style={textStyles.text} testID="ObsList.obsCard.commentCount">{numOfComments || 0}</Text>
|
||||
<Text style={textStyles.text}>{qualityGrade || "no quality grade"}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return type === "list" ? renderColumn( ) : renderRow( );
|
||||
};
|
||||
|
||||
export default ObsCardStats;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { FlatList, ActivityIndicator, View, Pressable, Text } from "react-native";
|
||||
import { FlatList, View, Pressable, Text } from "react-native";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { viewStyles } from "../../../styles/observations/obsList";
|
||||
|
||||
import { viewStyles, textStyles } from "../../../styles/observations/obsList";
|
||||
import GridItem from "./GridItem";
|
||||
import EmptyList from "./EmptyList";
|
||||
import ObsCard from "./ObsCard";
|
||||
import Map from "../Map";
|
||||
import InfiniteScrollFooter from "./InfiniteScrollFooter";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
observationList: Array<Object>,
|
||||
testID: string,
|
||||
taxonId?: number
|
||||
taxonId?: number,
|
||||
mapHeight?: number,
|
||||
totalObservations?: number,
|
||||
handleEndReached?: Function
|
||||
}
|
||||
|
||||
const ObservationViews = ( {
|
||||
loading,
|
||||
observationList,
|
||||
testID,
|
||||
taxonId
|
||||
taxonId,
|
||||
mapHeight,
|
||||
totalObservations,
|
||||
handleEndReached
|
||||
}: Props ): React.Node => {
|
||||
const [view, setView] = React.useState( "list" );
|
||||
const navigation = useNavigation( );
|
||||
@@ -39,54 +46,66 @@ const ObservationViews = ( {
|
||||
const setListView = ( ) => setView( "list" );
|
||||
const setMapView = ( ) => setView( "map" );
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const renderFooter = ( ) => loading ? <InfiniteScrollFooter /> : <View style={viewStyles.footer} />;
|
||||
|
||||
const renderView = ( ) => {
|
||||
if ( view === "map" ) {
|
||||
return <Map taxonId={taxonId} />;
|
||||
return <Map taxonId={taxonId} mapHeight={mapHeight} />;
|
||||
} else {
|
||||
return (
|
||||
<FlatList
|
||||
data={observationList}
|
||||
key={view === "grid" ? 1 : 0}
|
||||
renderItem={view === "grid" ? renderGridItem : renderItem}
|
||||
numColumns={view === "grid" ? 4 : 1}
|
||||
numColumns={view === "grid" ? 2 : 1}
|
||||
testID={testID}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
onEndReached={handleEndReached}
|
||||
ListFooterComponent={renderFooter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isExplore = name === "Explore";
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={viewStyles.toggleViewRow}>
|
||||
{name === "Explore" && (
|
||||
{isExplore && (
|
||||
<View style={[viewStyles.whiteBanner, view === "map" && viewStyles.greenBanner]}>
|
||||
<Text style={[textStyles.center, view === "map" && textStyles.whiteText]}>{t( "X-Observations", { observationCount: totalObservations } )}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={[viewStyles.toggleViewRow, isExplore ? viewStyles.exploreButtons : viewStyles.obsListButtons]}>
|
||||
{isExplore && (
|
||||
<Pressable
|
||||
onPress={setMapView}
|
||||
accessibilityRole="button"
|
||||
testID="Explore.toggleMapView"
|
||||
>
|
||||
<Text>map view</Text>
|
||||
<Text>map</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={setListView}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text>{ t( "List-View" ) }</Text>
|
||||
<Text>list</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={setGridView}
|
||||
testID="ObsList.toggleGridView"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text>{ t( "Grid-View" ) }</Text>
|
||||
<Text>grid</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
{loading
|
||||
{renderView( )}
|
||||
{/* {loading
|
||||
? <ActivityIndicator />
|
||||
: renderView( )}
|
||||
: renderView( )} */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
21
src/components/SharedComponents/TranslatedText.js
Normal file
21
src/components/SharedComponents/TranslatedText.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
style?: Object,
|
||||
count?: number
|
||||
}
|
||||
|
||||
const TranslatedText = ( { text, style, count }: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
|
||||
return <Text style={style}>{t( text, { count } )}</Text>;
|
||||
};
|
||||
|
||||
export default TranslatedText;
|
||||
@@ -6,13 +6,13 @@ import React, { useContext, useState, useEffect } from "react";
|
||||
import { Text, Pressable, View, Platform, PermissionsAndroid } from "react-native";
|
||||
// $FlowFixMe
|
||||
import AudioRecorderPlayer from "react-native-audio-recorder-player";
|
||||
import type { Node } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import uuid from "react-native-uuid";
|
||||
import { getUnixTime } from "date-fns";
|
||||
import { useUserLocation } from "../../sharedHooks/useUserLocation";
|
||||
import { formatDateAndTime } from "../../sharedHelpers/dateAndTime";
|
||||
import type { Node } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import uuid from "react-native-uuid";
|
||||
import { getUnixTime } from "date-fns";
|
||||
import { useUserLocation } from "../../sharedHooks/useUserLocation";
|
||||
import { formatDateAndTime } from "../../sharedHelpers/dateAndTime";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import { viewStyles, textStyles } from "../../styles/soundRecorder/soundRecorder";
|
||||
@@ -163,18 +163,6 @@ const SoundRecorder = ( ): Node => {
|
||||
setStatus( "paused" );
|
||||
};
|
||||
|
||||
const renderHelpText = ( ) => {
|
||||
if ( status === "notStarted" ) {
|
||||
return t( "Press-Record-to-Start" );
|
||||
} else if ( status === "recording" ) {
|
||||
return t( "Recording-Sound" );
|
||||
} else if ( status === "paused" ) {
|
||||
return ( t( "Paused" ) );
|
||||
} else if ( status === "playing" ) {
|
||||
return ( t( "Playing-Sound" ) );
|
||||
}
|
||||
};
|
||||
|
||||
const renderRecordButton = ( ) => {
|
||||
if ( status === "notStarted" ) {
|
||||
return (
|
||||
@@ -253,7 +241,6 @@ const SoundRecorder = ( ): Node => {
|
||||
<Text>insert visualization here</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={textStyles.alignCenter}>{renderHelpText( )}</Text>
|
||||
<View style={viewStyles.recordButtonRow}>
|
||||
{renderPlaybackButton( )}
|
||||
{renderRecordButton( )}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
// @flow
|
||||
// // @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
// import { useEffect, useState } from "react";
|
||||
// import inatjs from "inaturalistjs";
|
||||
|
||||
const FIELDS = {
|
||||
title: true,
|
||||
icon: true
|
||||
};
|
||||
// const FIELDS = {
|
||||
// title: true,
|
||||
// icon: true
|
||||
// };
|
||||
|
||||
const useNetworkSite = ( ): Array<Object> => {
|
||||
// const [projects, setProjects] = useState( [] );
|
||||
// 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, );
|
||||
}
|
||||
};
|
||||
// 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( );
|
||||
// fetchSite( );
|
||||
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [] );
|
||||
// return ( ) => {
|
||||
// isCurrent = false;
|
||||
// };
|
||||
// }, [] );
|
||||
|
||||
return [];
|
||||
};
|
||||
// return [];
|
||||
// };
|
||||
|
||||
export default useNetworkSite;
|
||||
// export default useNetworkSite;
|
||||
|
||||
@@ -1,12 +1,323 @@
|
||||
# Header for a block of text describing a taxon
|
||||
ABOUT-taxon-header = ABOUT
|
||||
# Label for a view that shows observations as a grid of photos
|
||||
Grid-View = Grid View
|
||||
# Label for a view that shows observations a list
|
||||
List-View = List View
|
||||
|
||||
Add-Date-Time = Add Date/Time
|
||||
|
||||
Add-Location = Add Location
|
||||
|
||||
Add-optional-notes = Add optional notes
|
||||
|
||||
Add-to-projects = Add to projects
|
||||
|
||||
All = All
|
||||
|
||||
All-observations = All observations
|
||||
|
||||
Amphibians = Amphibians
|
||||
|
||||
Arachnids = Arachnids
|
||||
|
||||
Birds = Birds
|
||||
|
||||
Cancel = Cancel
|
||||
|
||||
Captive-Cultivated = Captive/Cultivated
|
||||
|
||||
# Quality grade option
|
||||
Casual = Casual
|
||||
|
||||
Combine-Photos = Combine Photos
|
||||
|
||||
# Onboarding for users learning to group photos in the camera roll
|
||||
Combine-photos-onboarding = Combine photos into observations – make sure there is only one species per observation
|
||||
|
||||
CREATE-AN-OBSERVATION = CREATE AN OBSERVATION
|
||||
|
||||
Date = Date
|
||||
|
||||
Date-added-newest-to-oldest = Date added - newest to oldest
|
||||
|
||||
Date-added-oldest-to-newest = Date added - oldest to newest
|
||||
|
||||
DELETE-X-OBSERVATIONS = DELETE {$count ->
|
||||
[one] 1 OBSERVATION
|
||||
*[other] {$count} OBSERVATIONS
|
||||
}
|
||||
|
||||
Description-Tags = Description/Tags
|
||||
|
||||
Evidence = Evidence
|
||||
|
||||
Explore = Explore
|
||||
|
||||
Filters = Filters
|
||||
|
||||
Finish = Finish
|
||||
|
||||
Fish = Fish
|
||||
|
||||
Fungi = Fungi
|
||||
|
||||
Geoprivacy = Geoprivacy:
|
||||
|
||||
Group-Photos = Group Photos
|
||||
|
||||
Has-Photos = Has Photos
|
||||
|
||||
Has-Sounds = Has Sounds
|
||||
|
||||
High = High
|
||||
|
||||
IDENTIFICATION = IDENTIFICATION
|
||||
|
||||
Identification = Identification
|
||||
|
||||
Insects = Insects
|
||||
|
||||
Introduced = Introduced
|
||||
|
||||
Location = Location
|
||||
|
||||
Low = Low
|
||||
|
||||
Mammals = Mammals
|
||||
|
||||
Media = Media
|
||||
|
||||
Mollusks = Mollusks
|
||||
|
||||
# The following Month- strings are the months of the year (in month order, not alphabetical order)
|
||||
Month-January = January
|
||||
|
||||
Month-February = February
|
||||
|
||||
Month-March = March
|
||||
|
||||
Month-April = April
|
||||
|
||||
Month-May = May
|
||||
|
||||
Month-June = June
|
||||
|
||||
Month-July = July
|
||||
|
||||
Month-August = August
|
||||
|
||||
Month-September = September
|
||||
|
||||
Month-October = October
|
||||
|
||||
Month-November = November
|
||||
|
||||
Month-December = December
|
||||
|
||||
Months = Months
|
||||
|
||||
Most-faved = Most faved
|
||||
|
||||
Native = Native
|
||||
|
||||
# Quality grade option
|
||||
Needs-ID = Needs ID
|
||||
|
||||
New-Observation = New Observation
|
||||
|
||||
Next = Next
|
||||
|
||||
No-Location = No Location
|
||||
|
||||
Obscured = Obscured
|
||||
|
||||
Observation = Observation
|
||||
|
||||
Open = Open
|
||||
|
||||
Other-Data = Other Data
|
||||
|
||||
Paused = Paused
|
||||
|
||||
Photo-Licensing = Photo Licensing
|
||||
|
||||
Plants = Plants
|
||||
|
||||
# Help text for playing back a sound recording
|
||||
Playing-Sound = Playing Sound
|
||||
|
||||
# Help text for beginning a sound recording
|
||||
Press-Record-to-Start = Press Record to Start
|
||||
|
||||
Private = Private
|
||||
|
||||
Projects = Projects
|
||||
|
||||
Quality-Grade = Quality Grade
|
||||
|
||||
Rank = Rank
|
||||
|
||||
# The following Ranks- strings are taxonomic ranks (in taxonomic order, not alphabetical order)
|
||||
Ranks-stateofmatter = state of matter
|
||||
|
||||
Ranks-kingdom = kingdom
|
||||
|
||||
Ranks-subkingdom = subkingdom
|
||||
|
||||
Ranks-phylum = phylum
|
||||
|
||||
Ranks-subphylum = subphylum
|
||||
|
||||
Ranks-superclass = superclass
|
||||
|
||||
Ranks-class = class
|
||||
|
||||
Ranks-subclass = subclass
|
||||
|
||||
Ranks-infraclass = infraclass
|
||||
|
||||
Ranks-superorder = superorder
|
||||
|
||||
Ranks-order = order
|
||||
|
||||
Ranks-suborder = suborder
|
||||
|
||||
Ranks-infraorder = infraorder
|
||||
|
||||
Ranks-subterclass = subterclass
|
||||
|
||||
Ranks-parvorder = parvorder
|
||||
|
||||
Ranks-zoosection = zoosection
|
||||
|
||||
Ranks-zoosubsection = zoosubsection
|
||||
|
||||
Ranks-superfamily = superfamily
|
||||
|
||||
Ranks-epifamily = epifamily
|
||||
|
||||
Ranks-family = family
|
||||
|
||||
Ranks-subfamily = subfamily
|
||||
|
||||
Ranks-supertribe = supertribe
|
||||
|
||||
Ranks-tribe = tribe
|
||||
|
||||
Ranks-subtribe = subtribe
|
||||
|
||||
Ranks-genus = genus
|
||||
|
||||
Ranks-genushybrid = genushybrid
|
||||
|
||||
Ranks-subgenus = subgenus
|
||||
|
||||
Ranks-section = section
|
||||
|
||||
Ranks-subsection = subsection
|
||||
|
||||
Ranks-complex = complex
|
||||
|
||||
Ranks-species = species
|
||||
|
||||
Ranks-hybrid = hybrid
|
||||
|
||||
Ranks-subspecies = subspecies
|
||||
|
||||
Ranks-variety = variety
|
||||
|
||||
Ranks-form = form
|
||||
|
||||
Ranks-infrahybrid = infrahybrid
|
||||
|
||||
Recently-observed = Recently observed
|
||||
|
||||
Record-a-sound = Record a sound
|
||||
|
||||
Record-new-sound = Record new sound
|
||||
|
||||
Recording-Sound = Recording Sound
|
||||
|
||||
Remove-Photos = Remove Photos
|
||||
|
||||
Reptiles = Reptiles
|
||||
|
||||
# Quality grade option
|
||||
Research-Grade = Research Grade
|
||||
|
||||
Reset = Reset
|
||||
|
||||
Reviewed = Reviewed
|
||||
|
||||
Reviewed-only = Reviewed only
|
||||
|
||||
Search-for-a-location = Search for a location
|
||||
|
||||
Search-for-a-project = Search for a project
|
||||
|
||||
Search-for-a-taxon = Search for a taxon
|
||||
|
||||
Search-for-a-user = Search for a user
|
||||
|
||||
Search-for-description-tags-text = Search for description/tags text
|
||||
|
||||
Select = Select
|
||||
|
||||
Separate-Photos = Separate Photos
|
||||
|
||||
# Header for a section showing taxa similar to a single taxon
|
||||
SIMILAR-SPECIES-header
|
||||
SIMILAR-SPECIES-header = SIMILAR SPECIES
|
||||
|
||||
Sort-by = Sort by
|
||||
|
||||
Status = Status
|
||||
|
||||
# Header for a block of text describing a taxon's conservation status
|
||||
STATUS-header = STATUS
|
||||
|
||||
# Header in pop up explaining options for creating an observation
|
||||
STEP-1-EVIDENCE = STEP 1. EVIDENCE
|
||||
|
||||
Submit-without-evidence = Submit without evidence
|
||||
|
||||
Take-a-photo-with-your-camera = Take a photo with your camera
|
||||
|
||||
Tap-to-search-for-taxa = Tap to search for taxa
|
||||
|
||||
Taxon = Taxon
|
||||
|
||||
# Header for a block of text describing a taxon's taxonomy
|
||||
TAXONOMY-header = TAXONOMY
|
||||
|
||||
# Onboarding for users adding their first evidence of an organism
|
||||
The-first-thing-you-need-is-evidence = The first thing you need is evidence of an organism. This helps others identify what you saw.
|
||||
|
||||
Threatened = Threatened
|
||||
|
||||
Unreviewed-only = Unreviewed only
|
||||
|
||||
Upload-a-photo-from-your-gallery = Upload a photo from your gallery
|
||||
|
||||
UPLOAD-OBSERVATION = UPLOAD OBSERVATION
|
||||
|
||||
# Shows the number of photos a user selected from the camera roll for upload
|
||||
Upload-X-photos = Upload {$count ->
|
||||
[one] 1 photo
|
||||
*[other] {$count} photos
|
||||
}
|
||||
|
||||
User = User
|
||||
|
||||
Visually-search-iNaturalist-data = Visually search iNaturalist’s wealth of data. Search by a taxon in a location
|
||||
|
||||
# Banner above Explore Map showing total number of results
|
||||
X-Observations = {$observationCount ->
|
||||
[one] 1 Observation
|
||||
*[other] {$observationCount} Observations
|
||||
}
|
||||
|
||||
# Displays number of photos and observations a user has selected from the camera roll
|
||||
X-photos-X-observations = {$photoCount ->
|
||||
[one] 1 photo
|
||||
*[other] {$photoCount} photos
|
||||
}, {$observationCount ->
|
||||
[one] 1 observation
|
||||
*[other] {$observationCount} observations
|
||||
}
|
||||
@@ -3,22 +3,197 @@
|
||||
"comment": "Header for a block of text describing a taxon",
|
||||
"val": "ABOUT"
|
||||
},
|
||||
"Grid-View": {
|
||||
"comment": "Label for a view that shows observations as a grid of photos",
|
||||
"val": "Grid View"
|
||||
"Add-Date-Time": "Add Date/Time",
|
||||
"Add-Location": "Add Location",
|
||||
"Add-optional-notes": "Add optional notes",
|
||||
"Add-to-projects": "Add to projects",
|
||||
"All": "All",
|
||||
"All-observations": "All observations",
|
||||
"Amphibians": "Amphibians",
|
||||
"Arachnids": "Arachnids",
|
||||
"Birds": "Birds",
|
||||
"Cancel": "Cancel",
|
||||
"Captive-Cultivated": "Captive/Cultivated",
|
||||
"Casual": {
|
||||
"comment": "Quality grade option",
|
||||
"val": "Casual"
|
||||
},
|
||||
"List-View": {
|
||||
"comment": "Label for a view that shows observations a list",
|
||||
"val": "List View"
|
||||
"Combine-Photos": "Combine Photos",
|
||||
"Combine-photos-onboarding": {
|
||||
"comment": "Onboarding for users learning to group photos in the camera roll",
|
||||
"val": "Combine photos into observations – make sure there is only one species per observation"
|
||||
},
|
||||
"comment": "Header for a section showing taxa similar to a single taxon",
|
||||
"SIMILAR-SPECIES-header": "",
|
||||
"CREATE-AN-OBSERVATION": "CREATE AN OBSERVATION",
|
||||
"Date": "Date",
|
||||
"Date-added-newest-to-oldest": "Date added - newest to oldest",
|
||||
"Date-added-oldest-to-newest": "Date added - oldest to newest",
|
||||
"DELETE-X-OBSERVATIONS": "DELETE { $count ->\n [one] 1 OBSERVATION\n *[other] { $count } OBSERVATIONS\n}",
|
||||
"Description-Tags": "Description/Tags",
|
||||
"Evidence": "Evidence",
|
||||
"Explore": "Explore",
|
||||
"Filters": "Filters",
|
||||
"Finish": "Finish",
|
||||
"Fish": "Fish",
|
||||
"Fungi": "Fungi",
|
||||
"Geoprivacy": "Geoprivacy:",
|
||||
"Group-Photos": "Group Photos",
|
||||
"Has-Photos": "Has Photos",
|
||||
"Has-Sounds": "Has Sounds",
|
||||
"High": "High",
|
||||
"IDENTIFICATION": "IDENTIFICATION",
|
||||
"Identification": "Identification",
|
||||
"Insects": "Insects",
|
||||
"Introduced": "Introduced",
|
||||
"Location": "Location",
|
||||
"Low": "Low",
|
||||
"Mammals": "Mammals",
|
||||
"Media": "Media",
|
||||
"Mollusks": "Mollusks",
|
||||
"Month-January": {
|
||||
"comment": "The following Month- strings are the months of the year (in month order, not alphabetical order)",
|
||||
"val": "January"
|
||||
},
|
||||
"Month-February": "February",
|
||||
"Month-March": "March",
|
||||
"Month-April": "April",
|
||||
"Month-May": "May",
|
||||
"Month-June": "June",
|
||||
"Month-July": "July",
|
||||
"Month-August": "August",
|
||||
"Month-September": "September",
|
||||
"Month-October": "October",
|
||||
"Month-November": "November",
|
||||
"Month-December": "December",
|
||||
"Months": "Months",
|
||||
"Most-faved": "Most faved",
|
||||
"Native": "Native",
|
||||
"Needs-ID": {
|
||||
"comment": "Quality grade option",
|
||||
"val": "Needs ID"
|
||||
},
|
||||
"New-Observation": "New Observation",
|
||||
"Next": "Next",
|
||||
"No-Location": "No Location",
|
||||
"Obscured": "Obscured",
|
||||
"Observation": "Observation",
|
||||
"Open": "Open",
|
||||
"Other-Data": "Other Data",
|
||||
"Paused": "Paused",
|
||||
"Photo-Licensing": "Photo Licensing",
|
||||
"Plants": "Plants",
|
||||
"Playing-Sound": {
|
||||
"comment": "Help text for playing back a sound recording",
|
||||
"val": "Playing Sound"
|
||||
},
|
||||
"Press-Record-to-Start": {
|
||||
"comment": "Help text for beginning a sound recording",
|
||||
"val": "Press Record to Start"
|
||||
},
|
||||
"Private": "Private",
|
||||
"Projects": "Projects",
|
||||
"Quality-Grade": "Quality Grade",
|
||||
"Rank": "Rank",
|
||||
"Ranks-stateofmatter": {
|
||||
"comment": "The following Ranks- strings are taxonomic ranks (in taxonomic order, not alphabetical order)",
|
||||
"val": "state of matter"
|
||||
},
|
||||
"Ranks-kingdom": "kingdom",
|
||||
"Ranks-subkingdom": "subkingdom",
|
||||
"Ranks-phylum": "phylum",
|
||||
"Ranks-subphylum": "subphylum",
|
||||
"Ranks-superclass": "superclass",
|
||||
"Ranks-class": "class",
|
||||
"Ranks-subclass": "subclass",
|
||||
"Ranks-infraclass": "infraclass",
|
||||
"Ranks-superorder": "superorder",
|
||||
"Ranks-order": "order",
|
||||
"Ranks-suborder": "suborder",
|
||||
"Ranks-infraorder": "infraorder",
|
||||
"Ranks-subterclass": "subterclass",
|
||||
"Ranks-parvorder": "parvorder",
|
||||
"Ranks-zoosection": "zoosection",
|
||||
"Ranks-zoosubsection": "zoosubsection",
|
||||
"Ranks-superfamily": "superfamily",
|
||||
"Ranks-epifamily": "epifamily",
|
||||
"Ranks-family": "family",
|
||||
"Ranks-subfamily": "subfamily",
|
||||
"Ranks-supertribe": "supertribe",
|
||||
"Ranks-tribe": "tribe",
|
||||
"Ranks-subtribe": "subtribe",
|
||||
"Ranks-genus": "genus",
|
||||
"Ranks-genushybrid": "genushybrid",
|
||||
"Ranks-subgenus": "subgenus",
|
||||
"Ranks-section": "section",
|
||||
"Ranks-subsection": "subsection",
|
||||
"Ranks-complex": "complex",
|
||||
"Ranks-species": "species",
|
||||
"Ranks-hybrid": "hybrid",
|
||||
"Ranks-subspecies": "subspecies",
|
||||
"Ranks-variety": "variety",
|
||||
"Ranks-form": "form",
|
||||
"Ranks-infrahybrid": "infrahybrid",
|
||||
"Recently-observed": "Recently observed",
|
||||
"Record-a-sound": "Record a sound",
|
||||
"Record-new-sound": "Record new sound",
|
||||
"Recording-Sound": "Recording Sound",
|
||||
"Remove-Photos": "Remove Photos",
|
||||
"Reptiles": "Reptiles",
|
||||
"Research-Grade": {
|
||||
"comment": "Quality grade option",
|
||||
"val": "Research Grade"
|
||||
},
|
||||
"Reset": "Reset",
|
||||
"Reviewed": "Reviewed",
|
||||
"Reviewed-only": "Reviewed only",
|
||||
"Search-for-a-location": "Search for a location",
|
||||
"Search-for-a-project": "Search for a project",
|
||||
"Search-for-a-taxon": "Search for a taxon",
|
||||
"Search-for-a-user": "Search for a user",
|
||||
"Search-for-description-tags-text": "Search for description/tags text",
|
||||
"Select": "Select",
|
||||
"Separate-Photos": "Separate Photos",
|
||||
"SIMILAR-SPECIES-header": {
|
||||
"comment": "Header for a section showing taxa similar to a single taxon",
|
||||
"val": "SIMILAR SPECIES"
|
||||
},
|
||||
"Sort-by": "Sort by",
|
||||
"Status": "Status",
|
||||
"STATUS-header": {
|
||||
"comment": "Header for a block of text describing a taxon's conservation status",
|
||||
"val": "STATUS"
|
||||
},
|
||||
"STEP-1-EVIDENCE": {
|
||||
"comment": "Header in pop up explaining options for creating an observation",
|
||||
"val": "STEP 1. EVIDENCE"
|
||||
},
|
||||
"Submit-without-evidence": "Submit without evidence",
|
||||
"Take-a-photo-with-your-camera": "Take a photo with your camera",
|
||||
"Tap-to-search-for-taxa": "Tap to search for taxa",
|
||||
"Taxon": "Taxon",
|
||||
"TAXONOMY-header": {
|
||||
"comment": "Header for a block of text describing a taxon's taxonomy",
|
||||
"val": "TAXONOMY"
|
||||
},
|
||||
"The-first-thing-you-need-is-evidence": {
|
||||
"comment": "Onboarding for users adding their first evidence of an organism",
|
||||
"val": "The first thing you need is evidence of an organism. This helps others identify what you saw."
|
||||
},
|
||||
"Threatened": "Threatened",
|
||||
"Unreviewed-only": "Unreviewed only",
|
||||
"Upload-a-photo-from-your-gallery": "Upload a photo from your gallery",
|
||||
"UPLOAD-OBSERVATION": "UPLOAD OBSERVATION",
|
||||
"Upload-X-photos": {
|
||||
"comment": "Shows the number of photos a user selected from the camera roll for upload",
|
||||
"val": "Upload { $count ->\n [one] 1 photo\n *[other] { $count } photos\n}"
|
||||
},
|
||||
"User": "User",
|
||||
"Visually-search-iNaturalist-data": "Visually search iNaturalist’s wealth of data. Search by a taxon in a location",
|
||||
"X-Observations": {
|
||||
"comment": "Banner above Explore Map showing total number of results",
|
||||
"val": "{ $observationCount ->\n [one] 1 Observation\n *[other] { $observationCount } Observations\n}"
|
||||
},
|
||||
"X-photos-X-observations": {
|
||||
"comment": "Displays number of photos and observations a user has selected from the camera roll",
|
||||
"val": "{ $photoCount ->\n [one] 1 photo\n *[other] { $photoCount } photos\n}, { $observationCount ->\n [one] 1 observation\n *[other] { $observationCount } observations\n}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,143 @@
|
||||
# Header for a block of text describing a taxon
|
||||
ABOUT-taxon-header = ABOUT
|
||||
|
||||
Add-Date-Time = Add Date/Time
|
||||
|
||||
Add-Location = Add Location
|
||||
|
||||
Add-optional-notes = Add optional notes
|
||||
|
||||
Add-to-projects = Add to projects
|
||||
|
||||
All = All
|
||||
|
||||
All-observations = All observations
|
||||
|
||||
Amphibians = Amphibians
|
||||
|
||||
Arachnids = Arachnids
|
||||
|
||||
Birds = Birds
|
||||
|
||||
Cancel = Cancel
|
||||
|
||||
Captive-Cultivated = Captive/Cultivated
|
||||
|
||||
# Quality grade option
|
||||
Casual = Casual
|
||||
|
||||
Combine-Photos = Combine Photos
|
||||
|
||||
# Onboarding for users learning to group photos in the camera roll
|
||||
Combine-photos-onboarding = Combine photos into observations – make sure there is only one species per observation
|
||||
|
||||
CREATE-AN-OBSERVATION = CREATE AN OBSERVATION
|
||||
|
||||
Date = Date
|
||||
|
||||
Date-added-newest-to-oldest = Date added - newest to oldest
|
||||
|
||||
Date-added-oldest-to-newest = Date added - oldest to newest
|
||||
|
||||
DELETE-X-OBSERVATIONS = DELETE {$count ->
|
||||
[one] 1 OBSERVATION
|
||||
*[other] {$count} OBSERVATIONS
|
||||
}
|
||||
|
||||
Description-Tags = Description/Tags
|
||||
|
||||
Evidence = Evidence
|
||||
|
||||
Explore = Explore
|
||||
|
||||
Filters = Filters
|
||||
|
||||
Finish = Finish
|
||||
|
||||
Fish = Fish
|
||||
|
||||
Fungi = Fungi
|
||||
|
||||
# Label for a view that shows observations as a grid of photos
|
||||
Grid-View = Grid View
|
||||
Geoprivacy = Geoprivacy:
|
||||
|
||||
Group-Photos = Group Photos
|
||||
|
||||
Has-Photos = Has Photos
|
||||
|
||||
Has-Sounds = Has Sounds
|
||||
|
||||
High = High
|
||||
|
||||
IDENTIFICATION = IDENTIFICATION
|
||||
|
||||
Identification = Identification
|
||||
|
||||
Insects = Insects
|
||||
|
||||
# Label for a view that shows observations a list
|
||||
List-View = List View
|
||||
Introduced = Introduced
|
||||
|
||||
Location = Location
|
||||
|
||||
Low = Low
|
||||
|
||||
Mammals = Mammals
|
||||
|
||||
Media = Media
|
||||
|
||||
Mollusks = Mollusks
|
||||
|
||||
# The following Month- strings are the months of the year (in month order, not alphabetical order)
|
||||
Month-January = January
|
||||
|
||||
Month-February = February
|
||||
|
||||
Month-March = March
|
||||
|
||||
Month-April = April
|
||||
|
||||
Month-May = May
|
||||
|
||||
Month-June = June
|
||||
|
||||
Month-July = July
|
||||
|
||||
Month-August = August
|
||||
|
||||
Month-September = September
|
||||
|
||||
Month-October = October
|
||||
|
||||
Month-November = November
|
||||
|
||||
Month-December = December
|
||||
|
||||
Months = Months
|
||||
|
||||
Most-faved = Most faved
|
||||
|
||||
Native = Native
|
||||
|
||||
# Quality grade option
|
||||
Needs-ID = Needs ID
|
||||
|
||||
New-Observation = New Observation
|
||||
|
||||
Next = Next
|
||||
|
||||
No-Location = No Location
|
||||
|
||||
Obscured = Obscured
|
||||
|
||||
Observation = Observation
|
||||
|
||||
Open = Open
|
||||
|
||||
Other-Data = Other Data
|
||||
|
||||
Paused = Paused
|
||||
|
||||
Photo-Licensing = Photo Licensing
|
||||
|
||||
Plants = Plants
|
||||
|
||||
# Help text for playing back a sound recording
|
||||
@@ -43,17 +146,178 @@ Playing-Sound = Playing Sound
|
||||
# Help text for beginning a sound recording
|
||||
Press-Record-to-Start = Press Record to Start
|
||||
|
||||
Private = Private
|
||||
|
||||
Projects = Projects
|
||||
|
||||
Quality-Grade = Quality Grade
|
||||
|
||||
Rank = Rank
|
||||
|
||||
# The following Ranks- strings are taxonomic ranks (in taxonomic order, not alphabetical order)
|
||||
Ranks-stateofmatter = state of matter
|
||||
|
||||
Ranks-kingdom = kingdom
|
||||
|
||||
Ranks-subkingdom = subkingdom
|
||||
|
||||
Ranks-phylum = phylum
|
||||
|
||||
Ranks-subphylum = subphylum
|
||||
|
||||
Ranks-superclass = superclass
|
||||
|
||||
Ranks-class = class
|
||||
|
||||
Ranks-subclass = subclass
|
||||
|
||||
Ranks-infraclass = infraclass
|
||||
|
||||
Ranks-superorder = superorder
|
||||
|
||||
Ranks-order = order
|
||||
|
||||
Ranks-suborder = suborder
|
||||
|
||||
Ranks-infraorder = infraorder
|
||||
|
||||
Ranks-subterclass = subterclass
|
||||
|
||||
Ranks-parvorder = parvorder
|
||||
|
||||
Ranks-zoosection = zoosection
|
||||
|
||||
Ranks-zoosubsection = zoosubsection
|
||||
|
||||
Ranks-superfamily = superfamily
|
||||
|
||||
Ranks-epifamily = epifamily
|
||||
|
||||
Ranks-family = family
|
||||
|
||||
Ranks-subfamily = subfamily
|
||||
|
||||
Ranks-supertribe = supertribe
|
||||
|
||||
Ranks-tribe = tribe
|
||||
|
||||
Ranks-subtribe = subtribe
|
||||
|
||||
Ranks-genus = genus
|
||||
|
||||
Ranks-genushybrid = genushybrid
|
||||
|
||||
Ranks-subgenus = subgenus
|
||||
|
||||
Ranks-section = section
|
||||
|
||||
Ranks-subsection = subsection
|
||||
|
||||
Ranks-complex = complex
|
||||
|
||||
Ranks-species = species
|
||||
|
||||
Ranks-hybrid = hybrid
|
||||
|
||||
Ranks-subspecies = subspecies
|
||||
|
||||
Ranks-variety = variety
|
||||
|
||||
Ranks-form = form
|
||||
|
||||
Ranks-infrahybrid = infrahybrid
|
||||
|
||||
Recently-observed = Recently observed
|
||||
|
||||
Record-a-sound = Record a sound
|
||||
|
||||
Record-new-sound = Record new sound
|
||||
|
||||
Recording-Sound = Recording Sound
|
||||
|
||||
Remove-Photos = Remove Photos
|
||||
|
||||
Reptiles = Reptiles
|
||||
|
||||
# Quality grade option
|
||||
Research-Grade = Research Grade
|
||||
|
||||
Reset = Reset
|
||||
|
||||
Reviewed = Reviewed
|
||||
|
||||
Reviewed-only = Reviewed only
|
||||
|
||||
Search-for-a-location = Search for a location
|
||||
|
||||
Search-for-a-project = Search for a project
|
||||
|
||||
Search-for-a-taxon = Search for a taxon
|
||||
|
||||
Search-for-a-user = Search for a user
|
||||
|
||||
Search-for-description-tags-text = Search for description/tags text
|
||||
|
||||
Select = Select
|
||||
|
||||
Separate-Photos = Separate Photos
|
||||
|
||||
# Header for a section showing taxa similar to a single taxon
|
||||
SIMILAR-SPECIES-header
|
||||
SIMILAR-SPECIES-header = SIMILAR SPECIES
|
||||
|
||||
Sort-by = Sort by
|
||||
|
||||
Status = Status
|
||||
|
||||
# Header for a block of text describing a taxon's conservation status
|
||||
STATUS-header = STATUS
|
||||
|
||||
# Header in pop up explaining options for creating an observation
|
||||
STEP-1-EVIDENCE = STEP 1. EVIDENCE
|
||||
|
||||
Submit-without-evidence = Submit without evidence
|
||||
|
||||
Take-a-photo-with-your-camera = Take a photo with your camera
|
||||
|
||||
Tap-to-search-for-taxa = Tap to search for taxa
|
||||
|
||||
Taxon = Taxon
|
||||
|
||||
# Header for a block of text describing a taxon's taxonomy
|
||||
TAXONOMY-header = TAXONOMY
|
||||
|
||||
# Onboarding for users adding their first evidence of an organism
|
||||
The-first-thing-you-need-is-evidence = The first thing you need is evidence of an organism. This helps others identify what you saw.
|
||||
|
||||
Threatened = Threatened
|
||||
|
||||
Unreviewed-only = Unreviewed only
|
||||
|
||||
Upload-a-photo-from-your-gallery = Upload a photo from your gallery
|
||||
|
||||
UPLOAD-OBSERVATION = UPLOAD OBSERVATION
|
||||
|
||||
# Shows the number of photos a user selected from the camera roll for upload
|
||||
Upload-X-photos = Upload {$count ->
|
||||
[one] 1 photo
|
||||
*[other] {$count} photos
|
||||
}
|
||||
|
||||
User = User
|
||||
|
||||
Visually-search-iNaturalist-data = Visually search iNaturalist’s wealth of data. Search by a taxon in a location
|
||||
|
||||
# Banner above Explore Map showing total number of results
|
||||
X-Observations = {$observationCount ->
|
||||
[one] 1 Observation
|
||||
*[other] {$observationCount} Observations
|
||||
}
|
||||
|
||||
# Displays number of photos and observations a user has selected from the camera roll
|
||||
X-photos-X-observations = {$photoCount ->
|
||||
[one] 1 photo
|
||||
*[other] {$photoCount} photos
|
||||
}, {$observationCount ->
|
||||
[one] 1 observation
|
||||
*[other] {$observationCount} observations
|
||||
}
|
||||
@@ -70,7 +70,18 @@ class Observation {
|
||||
|
||||
// TODO: swap this and realm schema to use observation_photos everywhere, if possible
|
||||
// so there's no need for projectUri
|
||||
static uri = obs => ( obs && obs.observationPhotos && obs.observationPhotos[0] ) && { uri: obs.observationPhotos[0].photo.url };
|
||||
static uri = ( obs, medium ) => {
|
||||
let photoUri;
|
||||
if ( obs && obs.observationPhotos && obs.observationPhotos[0] ) {
|
||||
if ( medium ) {
|
||||
// need medium size for GridView component
|
||||
photoUri = obs.observationPhotos[0].photo.url.replace( "square", "medium" );
|
||||
} else {
|
||||
photoUri = obs.observationPhotos[0].photo.url;
|
||||
}
|
||||
}
|
||||
return { uri: photoUri };
|
||||
}
|
||||
|
||||
static projectUri = obs => {
|
||||
const photo = obs.observation_photos[0];
|
||||
|
||||
@@ -10,6 +10,7 @@ import PhotoGalleryProvider from "../providers/PhotoGalleryProvider";
|
||||
import SoundRecorder from "../components/SoundRecorder/SoundRecorder";
|
||||
import NormalCamera from "../components/Camera/NormalCamera";
|
||||
import CVSuggestions from "../components/ObsEdit/CVSuggestions";
|
||||
import CustomHeaderWithTranslation from "../components/SharedComponents/CustomHeaderWithTranslation";
|
||||
|
||||
const Stack = createNativeStackNavigator( );
|
||||
|
||||
@@ -43,6 +44,10 @@ const CameraStackNavigation = ( ): React.Node => (
|
||||
<Stack.Screen
|
||||
name="Suggestions"
|
||||
component={CVSuggestions}
|
||||
options={{
|
||||
headerTitle: ( props ) => <CustomHeaderWithTranslation {...props} headerText="IDENTIFICATION" />,
|
||||
headerShown: true
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</PhotoGalleryProvider>
|
||||
|
||||
@@ -2,43 +2,36 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
|
||||
import ExploreProvider from "../providers/ExploreProvider";
|
||||
import Explore from "../components/Explore/Explore";
|
||||
import ExploreFilters from "../components/Explore/ExploreFilters";
|
||||
import FiltersIcon from "../components/Explore/FiltersIcon";
|
||||
import ClearFiltersButton from "../components/Explore/ClearFilterButton";
|
||||
import ExploreLanding from "../components/Explore/ExploreLanding";
|
||||
|
||||
const Stack = createNativeStackNavigator( );
|
||||
|
||||
const screenOptions = {
|
||||
headerShown: true
|
||||
const hideHeader = {
|
||||
headerShown: false
|
||||
};
|
||||
|
||||
const ExploreStackNavigation = ( ): React.Node => (
|
||||
<ExploreProvider>
|
||||
<Stack.Navigator screenOptions={screenOptions}>
|
||||
{/* TODO: Figure out where Explore actually needs to live in navigator.
|
||||
It seems like it should be a tab navigator within the drawer navigator,
|
||||
which has stacks nested inside for Explore, ObsList, and Notifications
|
||||
and another tab navigator for Camera */}
|
||||
<Stack.Screen
|
||||
name="Explore"
|
||||
component={Explore}
|
||||
options={( { navigation } ) => ( {
|
||||
headerLeft: ( ) => <HeaderBackButton onPress={( ) => navigation.goBack( )} />,
|
||||
headerRight: ( ) => <FiltersIcon />
|
||||
} )}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ExploreFilters"
|
||||
component={ExploreFilters}
|
||||
options={( { navigation } ) => ( {
|
||||
headerLeft: ( ) => <HeaderBackButton onPress={( ) => navigation.goBack( )} />,
|
||||
headerRight: ( ) => <ClearFiltersButton />
|
||||
} )}
|
||||
/>
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
name="ExploreLanding"
|
||||
component={ExploreLanding}
|
||||
options={hideHeader}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Explore"
|
||||
component={Explore}
|
||||
options={hideHeader}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ExploreFilters"
|
||||
component={ExploreFilters}
|
||||
options={hideHeader}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</ExploreProvider>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import ObsList from "../components/Observations/ObsList";
|
||||
import ObsDetails from "../components/ObsDetails/ObsDetails";
|
||||
import UserProfile from "../components/UserProfile/UserProfile";
|
||||
import TaxonDetails from "../components/TaxonDetails/TaxonDetails";
|
||||
import MessagesIcon from "../components/Observations/MessagesIcon";
|
||||
import ObservationProvider from "../providers/ObservationProvider";
|
||||
import CustomHeaderWithTranslation from "../components/SharedComponents/CustomHeaderWithTranslation";
|
||||
|
||||
@@ -34,9 +33,7 @@ const MyObservationsStackNavigation = ( ): React.Node => (
|
||||
<Stack.Screen
|
||||
name="ObsList"
|
||||
component={ObsList}
|
||||
options={( { navigation } ) => ( {
|
||||
headerRight: ( ) => <MessagesIcon />
|
||||
} )}
|
||||
options={hideHeader}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ObsDetails"
|
||||
|
||||
@@ -16,6 +16,7 @@ import IdentifyStackNavigation from "./identifyStackNavigation";
|
||||
import ObsEditProvider from "../providers/ObsEditProvider";
|
||||
import NetworkLogging from "../components/NetworkLogging";
|
||||
import NotificationsStackNavigation from "./notificationsStackNavigation";
|
||||
import About from "../components/About";
|
||||
|
||||
// this removes the default hamburger menu from header
|
||||
const screenOptions = { headerLeft: ( ) => <></> };
|
||||
@@ -57,7 +58,10 @@ const App = ( ): React.Node => (
|
||||
/>
|
||||
<Drawer.Screen name="settings" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="following (dashboard)" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="about" component={PlaceholderComponent} />
|
||||
<Drawer.Screen
|
||||
name="about"
|
||||
component={About}
|
||||
/>
|
||||
<Drawer.Screen name="help/tutorials" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="login" component={Login} />
|
||||
<Drawer.Screen
|
||||
|
||||
@@ -11,22 +11,43 @@ type Props = {
|
||||
children: any
|
||||
}
|
||||
|
||||
const initialFilters = {
|
||||
d1: null,
|
||||
d2: null,
|
||||
month: null,
|
||||
place_id: null,
|
||||
project_id: null,
|
||||
quality_grade: null,
|
||||
sort_by: "observed_on",
|
||||
const initialOptions = {
|
||||
order: "desc",
|
||||
order_by: "created_at",
|
||||
taxon_id: null,
|
||||
place_id: null
|
||||
};
|
||||
|
||||
const initialFilters = {
|
||||
captive: false,
|
||||
hrank: [],
|
||||
introduced: false,
|
||||
lrank: [],
|
||||
months: [],
|
||||
native: false,
|
||||
photo_license: [],
|
||||
photos: true,
|
||||
project_id: null,
|
||||
// start by showing verifiable observations
|
||||
quality_grade: ["needs_id", "research"],
|
||||
sounds: false,
|
||||
threatened: false,
|
||||
user_id: null
|
||||
};
|
||||
|
||||
const ExploreProvider = ( { children }: Props ): Node => {
|
||||
const [exploreList, setExploreList] = useState( [] );
|
||||
const [exploreFilters, setExploreFilters] = useState( initialFilters );
|
||||
const [exploreFilters, setExploreFilters] = useState( {
|
||||
...initialOptions,
|
||||
...initialFilters
|
||||
} );
|
||||
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;
|
||||
@@ -38,18 +59,16 @@ const ExploreProvider = ( { children }: Props ): Node => {
|
||||
const filters = Object.fromEntries( Object.entries( exploreFilters ).filter( ( [_, v] ) => v != null ) );
|
||||
try {
|
||||
const params = {
|
||||
// TODO: note that there's a bug with place_id in API v2, so this is not working
|
||||
// as of Dec 20, 2021 with a place selected
|
||||
...filters,
|
||||
verifiable: true,
|
||||
photos: true,
|
||||
fields: 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 );
|
||||
@@ -66,7 +85,21 @@ const ExploreProvider = ( { children }: Props ): Node => {
|
||||
|
||||
const setLoading = ( ) => setLoadingExplore( true );
|
||||
|
||||
const clearFilters = ( ) => setExploreFilters( initialFilters );
|
||||
const resetFilters = ( ) => setExploreFilters( {
|
||||
...exploreFilters,
|
||||
...initialFilters
|
||||
} );
|
||||
|
||||
const applyFilters = ( ) => {
|
||||
setLoadingExplore( true );
|
||||
const applied = Object.assign( exploreFilters, unappliedFilters );
|
||||
console.log( applied, "applied" );
|
||||
setExploreFilters( applied );
|
||||
};
|
||||
|
||||
const resetUnappliedFilters = ( ) => setUnappliedFilters( {
|
||||
...initialFilters
|
||||
} );
|
||||
|
||||
const exploreValue = {
|
||||
exploreList,
|
||||
@@ -74,7 +107,16 @@ const ExploreProvider = ( { children }: Props ): Node => {
|
||||
setLoading,
|
||||
exploreFilters,
|
||||
setExploreFilters,
|
||||
clearFilters
|
||||
resetFilters,
|
||||
taxon,
|
||||
setTaxon,
|
||||
location,
|
||||
setLocation,
|
||||
totalObservations,
|
||||
unappliedFilters,
|
||||
setUnappliedFilters,
|
||||
applyFilters,
|
||||
resetUnappliedFilters
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// @flow
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import React from "react";
|
||||
import type { Node } from "react";
|
||||
import Realm from "realm";
|
||||
|
||||
import realmConfig from "../models/index";
|
||||
import { ObservationContext } from "./contexts";
|
||||
import useObservations from "./hooks/useObservations";
|
||||
|
||||
@@ -12,69 +10,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const ObservationProvider = ( { children }: Props ): Node => {
|
||||
const [observationList, setObservationList] = useState( [] );
|
||||
const [refetch, setRefetch] = useState( false );
|
||||
|
||||
const syncObservations = ( ) => setRefetch( true );
|
||||
|
||||
// TODO: put this fetch into either a sync button or a pull-from-top gesture
|
||||
// instead of automatically fetching every time ObsProvider loads
|
||||
// and add syncing logic to Realm schemas
|
||||
const loading = useObservations( refetch );
|
||||
|
||||
// We store a reference to our realm using useRef that allows us to access it via
|
||||
// realmRef.current for the component's lifetime without causing rerenders if updated.
|
||||
const realmRef = useRef( null );
|
||||
|
||||
const openRealm = useCallback( async ( ) => {
|
||||
// Since this is a non-sync realm, realm will be opened synchronously when calling "Realm.open"
|
||||
const realm = await Realm.open( realmConfig );
|
||||
realmRef.current = realm;
|
||||
|
||||
// When querying a realm to find objects (e.g. realm.objects('Observation')) the result we get back
|
||||
// and the objects in it are "live" and will always reflect the latest state.
|
||||
const localObservations = realm.objects( "Observation" );
|
||||
|
||||
if ( localObservations?.length ) {
|
||||
setObservationList( localObservations );
|
||||
}
|
||||
|
||||
try {
|
||||
localObservations.addListener( ( ) => {
|
||||
// If you just pass localObservations you end up assigning a Results
|
||||
// object to state instead of an array of observations. There's
|
||||
// probably a better way...
|
||||
setObservationList( localObservations.map( o => o ) );
|
||||
} );
|
||||
} catch ( err ) {
|
||||
console.error( "Unable to update local observations: ", err.message );
|
||||
}
|
||||
return ( ) => {
|
||||
// remember to remove listeners to avoid async updates
|
||||
localObservations.removeAllListeners( );
|
||||
realm.close( );
|
||||
};
|
||||
}, [realmRef, setObservationList] );
|
||||
|
||||
const closeRealm = useCallback( ( ) => {
|
||||
const realm = realmRef.current;
|
||||
realm?.close( );
|
||||
realmRef.current = null;
|
||||
setObservationList( [] );
|
||||
}, [realmRef] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
openRealm( );
|
||||
|
||||
// Return a cleanup callback to close the realm to prevent memory leaks
|
||||
return closeRealm;
|
||||
}, [openRealm, closeRealm] );
|
||||
|
||||
const observationValue = {
|
||||
observationList,
|
||||
loading,
|
||||
syncObservations
|
||||
};
|
||||
const observationValue = useObservations( );
|
||||
|
||||
return (
|
||||
<ObservationContext.Provider value={observationValue}>
|
||||
|
||||
@@ -20,11 +20,23 @@ const PhotoGalleryProvider = ( { children }: Props ): Node => {
|
||||
const [photoOptions, setPhotoOptions] = useState( options );
|
||||
// photos are fetched from the server on initial render
|
||||
// and anytime a user scrolls through the photo gallery
|
||||
const photosFetched = usePhotos( photoOptions, isScrolling );
|
||||
const photoFetchStatus = usePhotos( photoOptions, isScrolling );
|
||||
const photosFetched = photoFetchStatus.photos;
|
||||
const fetchingPhotos = photoFetchStatus.fetchingPhotos;
|
||||
|
||||
const [photoGallery, setPhotoGallery] = useState( {} );
|
||||
const [selectedPhotos, setSelectedPhotos] = useState( {} );
|
||||
|
||||
const totalSelected = ( ) => {
|
||||
let total = 0;
|
||||
const albums = Object.keys( selectedPhotos );
|
||||
|
||||
albums.forEach( album => {
|
||||
total += selectedPhotos[album].length;
|
||||
} );
|
||||
return total;
|
||||
};
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( photosFetched ) {
|
||||
// $FlowFixMe
|
||||
@@ -56,7 +68,9 @@ const PhotoGalleryProvider = ( { children }: Props ): Node => {
|
||||
photoOptions,
|
||||
setPhotoOptions,
|
||||
selectedPhotos,
|
||||
setSelectedPhotos
|
||||
setSelectedPhotos,
|
||||
fetchingPhotos,
|
||||
totalSelected: totalSelected( )
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,31 +9,68 @@ import Observation from "../../models/Observation";
|
||||
import { FIELDS } from "../helpers";
|
||||
import { getUsername } from "../../components/LoginSignUp/AuthenticationService";
|
||||
|
||||
const useObservations = ( refetch: boolean ): boolean => {
|
||||
const perPage = 6;
|
||||
|
||||
const useObservations = ( ): Object => {
|
||||
const [loading, setLoading] = useState( false );
|
||||
const [observationList, setObservationList] = useState( [] );
|
||||
const nextPageToFetch = observationList.length > 0 ? Math.ceil( observationList.length / perPage ) : 1;
|
||||
const [page, setPage] = useState( nextPageToFetch );
|
||||
const [userLogin, setUserLogin] = useState( null );
|
||||
|
||||
const syncObservations = ( username ) => {
|
||||
// await username on login screen for initial fetch
|
||||
setUserLogin( username );
|
||||
};
|
||||
|
||||
// We store a reference to our realm using useRef that allows us to access it via
|
||||
// realmRef.current for the component's lifetime without causing rerenders if updated.
|
||||
const realmRef = useRef( null );
|
||||
|
||||
const openRealm = useCallback( async ( ) => {
|
||||
// Since this is a non-sync realm, realm will be opened synchronously when calling "Realm.open"
|
||||
const realm = await Realm.open( realmConfig );
|
||||
realmRef.current = realm;
|
||||
|
||||
// When querying a realm to find objects (e.g. realm.objects('Observation')) the result we get back
|
||||
// and the objects in it are "live" and will always reflect the latest state.
|
||||
const localObservations = realm.objects( "Observation" );
|
||||
|
||||
if ( localObservations?.length ) {
|
||||
setObservationList( localObservations );
|
||||
}
|
||||
|
||||
try {
|
||||
const realm = await Realm.open( realmConfig );
|
||||
realmRef.current = realm;
|
||||
localObservations.addListener( ( ) => {
|
||||
// If you just pass localObservations you end up assigning a Results
|
||||
// object to state instead of an array of observations. There's
|
||||
// probably a better way...
|
||||
setObservationList( localObservations.map( o => o ) );
|
||||
} );
|
||||
} catch ( err ) {
|
||||
console.error( "Unable to update local observations 1: ", err.message );
|
||||
}
|
||||
catch ( err ) {
|
||||
console.error( "Error opening realm: ", err.message );
|
||||
}
|
||||
}, [realmRef] );
|
||||
return ( ) => {
|
||||
// remember to remove listeners to avoid async updates
|
||||
localObservations.removeAllListeners( );
|
||||
realm.close( );
|
||||
};
|
||||
}, [realmRef, setObservationList] );
|
||||
|
||||
const closeRealm = useCallback( ( ) => {
|
||||
const realm = realmRef.current;
|
||||
realm?.close( );
|
||||
realmRef.current = null;
|
||||
setObservationList( [] );
|
||||
}, [realmRef] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
openRealm( );
|
||||
|
||||
// Return a cleanup callback to close the realm to prevent memory leaks
|
||||
return closeRealm;
|
||||
// TODO: I think we need a cleanup function here to prevent memory leaks, but when we have it,
|
||||
// this error basically prevents the app from loading with a black screen of death
|
||||
// 'Exception in HostFunction: Cannot access realm that has been closed'
|
||||
// return closeRealm;
|
||||
}, [openRealm, closeRealm] );
|
||||
|
||||
const writeToDatabase = useCallback( ( results ) => {
|
||||
@@ -54,14 +91,15 @@ const useObservations = ( refetch: boolean ): boolean => {
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchObservations = async ( ) => {
|
||||
const userLogin = await getUsername( );
|
||||
console.log( userLogin, "user login fetch observations" );
|
||||
if ( !userLogin ) { return; }
|
||||
const username = await getUsername( );
|
||||
console.log( userLogin || username, "user login fetch observations for page: ", page );
|
||||
if ( !userLogin && !username ) { return; }
|
||||
setLoading( true );
|
||||
try {
|
||||
const params = {
|
||||
user_id: userLogin,
|
||||
per_page: 100,
|
||||
user_id: userLogin || username,
|
||||
page,
|
||||
per_page: perPage,
|
||||
fields: FIELDS
|
||||
};
|
||||
const response = await inatjs.observations.search( params );
|
||||
@@ -79,9 +117,16 @@ const useObservations = ( refetch: boolean ): boolean => {
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [writeToDatabase, refetch] );
|
||||
}, [writeToDatabase, page, userLogin] );
|
||||
|
||||
return loading;
|
||||
const fetchNextObservations = ( ) => setPage( page + 1 );
|
||||
|
||||
return {
|
||||
loading,
|
||||
observationList,
|
||||
syncObservations,
|
||||
fetchNextObservations
|
||||
};
|
||||
};
|
||||
|
||||
export default useObservations;
|
||||
|
||||
38
src/sharedHooks/useLoggedIn.js
Normal file
38
src/sharedHooks/useLoggedIn.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getUsername } from "../components/LoginSignUp/AuthenticationService";
|
||||
|
||||
const useLoggedIn = ( ): ?boolean => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState( null );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchLoggedInUser = async ( ) => {
|
||||
try {
|
||||
const currentUserLogin = await getUsername( );
|
||||
if ( !isCurrent ) { return; }
|
||||
if ( currentUserLogin ) {
|
||||
setIsLoggedIn( true );
|
||||
} else {
|
||||
setIsLoggedIn( false );
|
||||
}
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't check whether user logged in:", e.message );
|
||||
}
|
||||
};
|
||||
|
||||
fetchLoggedInUser( );
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [] );
|
||||
|
||||
return isLoggedIn;
|
||||
};
|
||||
|
||||
export {
|
||||
useLoggedIn
|
||||
};
|
||||
@@ -49,7 +49,11 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
|
||||
whiteText: {
|
||||
color: colors.white,
|
||||
zIndex: 1,
|
||||
fontSize: 24
|
||||
}
|
||||
} );
|
||||
|
||||
const imageStyles: { [string]: ImageStyleProp } = StyleSheet.create( {
|
||||
|
||||
@@ -13,17 +13,29 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
borderWidth: 0.5,
|
||||
height: 37,
|
||||
paddingLeft: 15,
|
||||
marginHorizontal: 50,
|
||||
marginVertical: 20,
|
||||
width: "75%"
|
||||
marginVertical: 5
|
||||
},
|
||||
positionBottom: {
|
||||
bottom: 140,
|
||||
width: "100%",
|
||||
position: "absolute"
|
||||
},
|
||||
bottomCard: {
|
||||
backgroundColor: colors.white,
|
||||
borderTopRightRadius: 30,
|
||||
borderTopLeftRadius: 30,
|
||||
borderColor: colors.gray,
|
||||
borderWidth: 1,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 70,
|
||||
paddingHorizontal: 20
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
explanation: {
|
||||
color: colors.gray,
|
||||
textAlign: "center",
|
||||
marginVertical: 10
|
||||
margin: 10
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import type { TextStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import type { TextStyleProp, ViewStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import { colors } from "../global";
|
||||
|
||||
const pickerSelectStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
inputIOS: {
|
||||
@@ -29,6 +30,40 @@ const pickerSelectStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
}
|
||||
} );
|
||||
|
||||
const checkboxWidth = 18;
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
checkbox: {
|
||||
width: checkboxWidth,
|
||||
height: checkboxWidth,
|
||||
padding: 10
|
||||
},
|
||||
checkboxRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap"
|
||||
},
|
||||
filtersRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: colors.lightGray,
|
||||
paddingVertical: 20
|
||||
},
|
||||
radioButtonBox: {
|
||||
borderWidth: 0
|
||||
},
|
||||
bottomPadding: {
|
||||
padding: 140
|
||||
},
|
||||
footer: {
|
||||
height: 100,
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
backgroundColor: colors.white
|
||||
}
|
||||
} );
|
||||
|
||||
export {
|
||||
pickerSelectStyles
|
||||
pickerSelectStyles,
|
||||
viewStyles
|
||||
};
|
||||
|
||||
@@ -4,5 +4,6 @@ export const colors = {
|
||||
white: "#ffffff",
|
||||
black: "#000000",
|
||||
inatGreen: "#77b300",
|
||||
gray: "#393939"
|
||||
gray: "#393939",
|
||||
lightGray: "#f5f5f5"
|
||||
};
|
||||
|
||||
@@ -63,6 +63,25 @@ const imageStyles: { [string]: ImageStyleProp } = StyleSheet.create( {
|
||||
} );
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
bottomModal: {
|
||||
backgroundColor: colors.white,
|
||||
borderTopRightRadius: 30,
|
||||
borderTopLeftRadius: 30,
|
||||
borderColor: colors.gray,
|
||||
borderWidth: 1,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 20,
|
||||
paddingHorizontal: 20,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
width: "100%"
|
||||
},
|
||||
noMargin: {
|
||||
margin: 0
|
||||
},
|
||||
saveButton: {
|
||||
width: 100
|
||||
},
|
||||
greenSelectionBorder: {
|
||||
borderWidth: 5,
|
||||
borderColor: colors.inatGreen
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { ViewStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
messages: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
marginRight: 20
|
||||
}
|
||||
} );
|
||||
|
||||
@@ -3,18 +3,47 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import type { ViewStyleProp, TextStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import { colors } from "../global";
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
toggleViewRow: {
|
||||
width: 140,
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-around",
|
||||
marginVertical: 20
|
||||
paddingVertical: 20,
|
||||
position: "absolute"
|
||||
},
|
||||
exploreButtons: {
|
||||
borderRadius: 40,
|
||||
borderWidth: 1,
|
||||
top: 400,
|
||||
zIndex: 1,
|
||||
backgroundColor: colors.white
|
||||
},
|
||||
obsListButtons: {
|
||||
right: 0,
|
||||
top: 100
|
||||
},
|
||||
greenBanner: {
|
||||
paddingVertical: 20,
|
||||
backgroundColor: colors.inatGreen
|
||||
},
|
||||
whiteBanner: {
|
||||
paddingVertical: 20
|
||||
},
|
||||
footer: {
|
||||
paddingTop: 100
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
text: { }
|
||||
center: {
|
||||
alignSelf: "center"
|
||||
},
|
||||
whiteText: {
|
||||
color: colors.white
|
||||
}
|
||||
} );
|
||||
|
||||
export {
|
||||
|
||||
26
src/styles/observations/userCard.js
Normal file
26
src/styles/observations/userCard.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow strict-local
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import type { ViewStyleProp, TextStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
userCard: {
|
||||
flexDirection: "row",
|
||||
height: 100,
|
||||
marginHorizontal: 20,
|
||||
alignItems: "center"
|
||||
},
|
||||
userDetails: {
|
||||
marginLeft: 10
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
text: { }
|
||||
} );
|
||||
|
||||
export {
|
||||
viewStyles,
|
||||
textStyles
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import { colors } from "../global";
|
||||
import type { ViewStyleProp, TextStyleProp, ImageStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
|
||||
const pickerContainer = {
|
||||
@@ -33,12 +34,30 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
height: 70,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
},
|
||||
selectionModal: {
|
||||
padding: 20,
|
||||
backgroundColor: colors.white,
|
||||
position: "absolute",
|
||||
bottom: 100
|
||||
},
|
||||
nextButton: {
|
||||
width: 100
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
header: {
|
||||
marginLeft: 10
|
||||
},
|
||||
text: {
|
||||
margin: 10
|
||||
},
|
||||
selections: {
|
||||
marginVertical: 10
|
||||
},
|
||||
disabled: {
|
||||
color: colors.lightGray
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
paddingVertical: 10,
|
||||
width: "80%",
|
||||
alignSelf: "center"
|
||||
},
|
||||
disabled: {
|
||||
backgroundColor: colors.lightGray
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import type { TextStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import type { TextStyleProp, ViewStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import { colors } from "../global";
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
whiteModal: {
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 40,
|
||||
padding: 20
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
whiteText: {
|
||||
color: colors.white,
|
||||
@@ -13,5 +21,6 @@ const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
} );
|
||||
|
||||
export {
|
||||
textStyles
|
||||
textStyles,
|
||||
viewStyles
|
||||
};
|
||||
|
||||
@@ -7,12 +7,13 @@ import { colors } from "../../global";
|
||||
|
||||
const { width } = Dimensions.get( "screen" );
|
||||
|
||||
const imageWidth = width / 3;
|
||||
const imageWidth = width / 2 - 20;
|
||||
const userImageWidth = 30;
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
gridItem: {
|
||||
width: imageWidth
|
||||
width: imageWidth,
|
||||
marginHorizontal: 10
|
||||
},
|
||||
taxonName: {
|
||||
height: 100
|
||||
@@ -23,17 +24,25 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
markReviewed: {
|
||||
backgroundColor: colors.gray,
|
||||
opacity: 0.5
|
||||
},
|
||||
totalObsPhotos: {
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
backgroundColor: colors.inatGreen,
|
||||
padding: 10,
|
||||
width: 40,
|
||||
zIndex: 1
|
||||
}
|
||||
} );
|
||||
|
||||
const textStyles: { [string]: TextStyleProp } = StyleSheet.create( {
|
||||
text: { }
|
||||
} );
|
||||
|
||||
const imageStyles: { [string]: ImageStyleProp } = StyleSheet.create( {
|
||||
gridImage: {
|
||||
width: imageWidth,
|
||||
height: imageWidth
|
||||
height: imageWidth,
|
||||
backgroundColor: colors.black
|
||||
},
|
||||
userImage: {
|
||||
borderRadius: 50,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @flow strict-local
|
||||
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import { colors } from "../../global";
|
||||
import type { ViewStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
|
||||
const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
infiniteScroll: {
|
||||
height: 100,
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.white,
|
||||
borderBottomColor: colors.lightGray,
|
||||
borderBottomWidth: 1
|
||||
}
|
||||
} );
|
||||
|
||||
export {
|
||||
viewStyles
|
||||
};
|
||||
@@ -5,7 +5,9 @@ import { StyleSheet, Dimensions } from "react-native";
|
||||
import type { ViewStyleProp, TextStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
|
||||
import { colors } from "../../global";
|
||||
|
||||
const { height } = Dimensions.get( "screen" );
|
||||
const { height, width } = Dimensions.get( "screen" );
|
||||
|
||||
const imageWidth = width / 2 - 20;
|
||||
|
||||
// safe area heights: https://stackoverflow.com/questions/46376860/what-is-the-safe-region-for-iphone-x-in-pixels-that-factors-the-top-notch-an/49174154
|
||||
const safeAreaViewPortraitMode = 78;
|
||||
@@ -34,6 +36,15 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
|
||||
photoContainer: {
|
||||
backgroundColor: colors.black,
|
||||
height: 200
|
||||
},
|
||||
photoStatRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-between",
|
||||
position: "absolute",
|
||||
bottom: 80,
|
||||
width: imageWidth,
|
||||
backgroundColor: colors.white
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React from "react";
|
||||
import factory, { makeResponse } from "../factory";
|
||||
import { render, waitFor, within } from "@testing-library/react-native";
|
||||
import { render, waitFor } from "@testing-library/react-native";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import AccessibilityEngine from "react-native-accessibility-engine";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user