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:
Amanda Bullington
2022-04-01 15:46:51 -07:00
committed by GitHub
parent 5a9aeb95ec
commit 1ebf4b951d
104 changed files with 4691 additions and 2074 deletions

2
.bundle/config Normal file
View File

@@ -0,0 +1,2 @@
BUNDLE_PATH: "vendor/bundle"
BUNDLE_FORCE_RUBY_PLATFORM: 1

View File

@@ -60,4 +60,4 @@ untyped-import
untyped-type-import
[version]
^0.149.0
^0.162.0

3
.gitattributes vendored
View File

@@ -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
View File

@@ -58,6 +58,9 @@ buck-out/
# CocoaPods
/ios/Pods/
# Ruby gems
vendor/
# Realm
*.realm*

1
.ruby-version Normal file
View File

@@ -0,0 +1 @@
2.7.4

4
Gemfile Normal file
View 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
View 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

View File

@@ -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 {

View 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>

View File

@@ -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>

View File

@@ -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' }

View File

@@ -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.

View File

@@ -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

View File

@@ -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'

View File

@@ -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();

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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>

View File

@@ -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
View File

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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;

View File

@@ -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}
>

View 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;

View File

@@ -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}

View 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;

View 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;

View File

@@ -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>
);
};

View 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;

View File

@@ -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;

View File

@@ -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}
/>
);

View File

@@ -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>
);
};

View File

@@ -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 />
</>
);
};

View 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;

View 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;

View 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;

View 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;

View File

@@ -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";

View File

@@ -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
};

View File

@@ -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

View File

@@ -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

View 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;

View File

@@ -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"
/>

View 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;

View 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;

View File

@@ -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>
);
};

View File

@@ -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 ) {

View File

@@ -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;

View File

@@ -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>
);

View 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;

View 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
};

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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" />
</>
);
};

View File

@@ -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>

View File

@@ -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}

View File

@@ -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;

View File

@@ -18,6 +18,7 @@ const ProjectDetails = ( ): React.Node => {
<ViewWithFooter>
<ImageBackground
source={{ uri: project.header_image_url }}
// $FlowFixMe
style={imageStyles.headerImage}
testID="ProjectDetails.headerImage"
>

View File

@@ -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>
);

View 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;

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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( )} */}
</>
);
};

View 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;

View File

@@ -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( )}

View File

@@ -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;

View File

@@ -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 iNaturalists 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
}

View File

@@ -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 iNaturalists 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}"
}
}

View File

@@ -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 iNaturalists 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
}

View File

@@ -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];

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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"

View File

@@ -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

View File

@@ -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 (

View File

@@ -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}>

View File

@@ -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 (

View File

@@ -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;

View 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
};

View File

@@ -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( {

View File

@@ -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
}
} );

View File

@@ -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
};

View File

@@ -4,5 +4,6 @@ export const colors = {
white: "#ffffff",
black: "#000000",
inatGreen: "#77b300",
gray: "#393939"
gray: "#393939",
lightGray: "#f5f5f5"
};

View File

@@ -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

View File

@@ -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
}
} );

View File

@@ -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 {

View 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
};

View File

@@ -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
}
} );

View File

@@ -13,6 +13,9 @@ const viewStyles: { [string]: ViewStyleProp } = StyleSheet.create( {
paddingVertical: 10,
width: "80%",
alignSelf: "center"
},
disabled: {
backgroundColor: colors.lightGray
}
} );

View File

@@ -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
};

View File

@@ -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,

View File

@@ -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
};

View File

@@ -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
}
} );

View File

@@ -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