mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Adopted and enforced code style from other iNat Javascript projects
These rules are largely based on the AirBnB ones, which are not quite standard for the React Native world, where Prettier seems to be more common, but I think they add a lot of useful checks, and unlike Prettier we can customize them. This also just makes it easier for people on the iNat team to work on the mobile app. Some specific changes: * Added eslint-plugin-react-hooks to eslint rules * Added eslint-plugin-simple-import-sort to eslint rules * Bugfix: could not import photo from gallery * Added support for react-native/no-inline-styles eslint rule * useUser should not bother fetching a user for a blank userId
This commit is contained in:
78
.eslintrc.js
78
.eslintrc.js
@@ -1,22 +1,88 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@react-native-community", "plugin:i18next/recommended"],
|
||||
parser: "@babel/eslint-parser",
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
babelOptions: {
|
||||
presets: ["@babel/preset-react"]
|
||||
}
|
||||
},
|
||||
extends: ["airbnb", "plugin:i18next/recommended"],
|
||||
plugins: [
|
||||
"react-hooks",
|
||||
"react-native",
|
||||
"simple-import-sort"
|
||||
],
|
||||
globals: {
|
||||
FormData: true
|
||||
},
|
||||
rules: {
|
||||
quotes: [2, "double"],
|
||||
"arrow-parens": [2, "as-needed"],
|
||||
"comma-dangle": [2, "never"],
|
||||
"space-in-parens": [2, "always"],
|
||||
"prettier/prettier": 0,
|
||||
"consistent-return": [2, { treatUndefinedAsUnspecified: true }],
|
||||
"func-names": 0,
|
||||
"global-require": 0,
|
||||
"i18next/no-literal-string": [2, {
|
||||
words: {
|
||||
// Minor change to the default to disallow all-caps string literals as well
|
||||
exclude: ["[0-9!-/:-@[-`{-~]+"]
|
||||
}
|
||||
}],
|
||||
"no-var": 1
|
||||
// The AirBNB approach at
|
||||
// https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/imports.js#L71
|
||||
// is quite particular and forbids imports of devDependencies anywhere
|
||||
// outside of tests and conventional config files, which causes problem
|
||||
// if you have, say, some utility scripts that have nothing to do with
|
||||
// your actually application. Here I'm trying to use the defaults
|
||||
// (https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-extraneous-dependencies.md,
|
||||
// which allow imports of anything declared in package.json, but should
|
||||
// raise alarms when you try to import things not declared in
|
||||
// package.json.
|
||||
"import/no-extraneous-dependencies": ["error", {}],
|
||||
"max-len": ["error", 100, 2, {
|
||||
ignoreUrls: true,
|
||||
ignoreComments: false,
|
||||
ignoreRegExpLiterals: true,
|
||||
ignoreStrings: false,
|
||||
ignoreTemplateLiterals: false
|
||||
}],
|
||||
"no-alert": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-unused-vars": ["error", {
|
||||
vars: "all",
|
||||
args: "after-used",
|
||||
// Overriding airbnb to allow leading underscore to indicate unused var
|
||||
argsIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true
|
||||
}],
|
||||
"no-void": 0,
|
||||
"prefer-destructuring": [2, { object: true, array: false }],
|
||||
quotes: [2, "double"],
|
||||
"space-in-parens": [2, "always"],
|
||||
"no-restricted-globals": 0,
|
||||
"no-param-reassign": 0,
|
||||
"no-var": 1,
|
||||
"prefer-const": [2, { destructuring: "all" }],
|
||||
// "react/forbid-prop-types": 0,
|
||||
"react/prop-types": 0,
|
||||
"react/destructuring-assignment": 0,
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react/function-component-definition": [2, { namedComponents: "arrow-function" }],
|
||||
"react/require-default-props": 0,
|
||||
|
||||
// React-Hooks Plugin
|
||||
// The following rules are made available via `eslint-plugin-react-hooks`
|
||||
"react-hooks/rules-of-hooks": 2,
|
||||
"react-hooks/exhaustive-deps": 2,
|
||||
|
||||
"react-native/no-inline-styles": "error",
|
||||
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error"
|
||||
},
|
||||
// need this so jest doesn't show as undefined in jest.setup.js
|
||||
env: {
|
||||
"jest": true
|
||||
jest: true
|
||||
},
|
||||
ignorePatterns: ["/coverage/*"]
|
||||
};
|
||||
|
||||
13
index.js
13
index.js
@@ -1,15 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import "react-native-gesture-handler";
|
||||
|
||||
import { AppRegistry } from "react-native";
|
||||
import inatjs from "inaturalistjs";
|
||||
import Config from "react-native-config";
|
||||
import App from "./src/navigation/rootNavigation";
|
||||
import {name as appName} from "./app.json";
|
||||
import "./src/i18n";
|
||||
|
||||
import inatjs from "inaturalistjs";
|
||||
import { AppRegistry } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import { startNetworkLogging } from "react-native-network-logger";
|
||||
|
||||
import { name as appName } from "./app.json";
|
||||
import App from "./src/navigation/rootNavigation";
|
||||
|
||||
startNetworkLogging();
|
||||
|
||||
// Configure inatjs to use the chosen URLs
|
||||
|
||||
@@ -559,7 +559,7 @@ SPEC CHECKSUMS:
|
||||
FBReactNativeSpec: 81ce99032d5b586fddd6a38d450f8595f7e04be4
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
glog: 476ee3e89abb49e07f822b48323c51c57124b572
|
||||
Permission-LocationWhenInUse: 006c85c8de0c05b5d8be8e8029e4f6b813270293
|
||||
Permission-LocationWhenInUse: 50736c907dff5a77d80a3503d3b333831148288b
|
||||
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
|
||||
RCTRequired: 3e917ea5377751094f38145fdece525aa90545a0
|
||||
RCTTypeSafety: c43c072a4bd60feb49a9570b0517892b4305c45e
|
||||
@@ -573,15 +573,15 @@ SPEC CHECKSUMS:
|
||||
React-jsiexecutor: b7b553412f2ec768fe6c8f27cd6bafdb9d8719e6
|
||||
React-jsinspector: c5989c77cb89ae6a69561095a61cce56a44ae8e8
|
||||
React-logger: a0833912d93b36b791b7a521672d8ee89107aff1
|
||||
react-native-cameraroll: 2957f2bce63ae896a848fbe0d5352c1bd4d20866
|
||||
react-native-cameraroll: 60ac50a5209777cbccfe8d7a62d0743a9da87060
|
||||
react-native-config: 6502b1879f97ed5ac570a029961fc35ea606cd14
|
||||
react-native-geocoder-reborn: c31cbc630d9307ebbceea1dea2746d0054be35c4
|
||||
react-native-geolocation-service: c0efb872258ed9240f1003a70fca9e9757e5c785
|
||||
react-native-image-picker: cffb727cf2f59bd5c0408e30b3dbe0b935f88835
|
||||
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
|
||||
react-native-image-picker: 9f9eb160a4f4fc0702f9d0a60fa453b413258ef8
|
||||
react-native-image-resizer: 506412a2bdd70dde64a61e13505ce10f61a04369
|
||||
react-native-maps: 8b8bfada2c86205a7f5a07dd1f92f29b33ea83aa
|
||||
react-native-netinfo: 3671b091c4843fda5e153612866ef4024b8f5d62
|
||||
react-native-safe-area-context: f98b0b16d1546d208fc293b4661e3f81a895afd9
|
||||
react-native-netinfo: ebbcd8fbe1a0ce7035e43cd18c5a545dcb93dd08
|
||||
react-native-safe-area-context: a95ad1a0e18341f58c5d6f53b0e2d43efade1a67
|
||||
react-native-sensitive-info: d44e909d065f9c0e15734245e5dd6a24b82e3dcd
|
||||
React-perflogger: a18b4f0bd933b8b24ecf9f3c54f9bf65180f3fe6
|
||||
React-RCTActionSheet: 547fe42fdb4b6089598d79f8e1d855d7c23e2162
|
||||
@@ -596,20 +596,20 @@ SPEC CHECKSUMS:
|
||||
React-runtimeexecutor: b960b687d2dfef0d3761fbb187e01812ebab8b23
|
||||
ReactCommon: 095366164a276d91ea704ce53cb03825c487a3f2
|
||||
RealmJS: 772520fb85c19b65c2ea0c8f9aa6e790a905a377
|
||||
RNAudioRecorderPlayer: 001f01049754e978c43af97162452c8563d5794a
|
||||
RNAudioRecorderPlayer: 4482e03d9336c8d8ba41825af227f74f41ba0a01
|
||||
RNCAsyncStorage: d81ee5c3db1060afd49ea7045ad460eff82d2b7d
|
||||
RNCCheckbox: ed1b4ca295475b41e7251ebae046360a703b6eb5
|
||||
RNCCheckbox: 934e11d33abb6b0db105cfa5a22a42bd89dfb4b9
|
||||
RNCPicker: abc646b53a3d28ccfa3232c927a0ca52e0cf024d
|
||||
RNDateTimePicker: 04b805a3cb4d386e5e6aff54b47ace7bad706fda
|
||||
RNDeviceInfo: aad3c663b25752a52bf8fce93f2354001dd185aa
|
||||
RNDateTimePicker: 47ab186e06826a4af7051b8a8199fac9477f339e
|
||||
RNDeviceInfo: af47a7d1a09d64c8cbb8b0c883db9bca422a9813
|
||||
RNFS: fc610f78fdf8bfc89a9e5cc2f898519f4dba1002
|
||||
RNGestureHandler: 4f4986408310a43f1606c391f38f76e0d6e790d5
|
||||
RNLocalize: cbcb55d0e19c78086ea4eea20e03fe8000bbbced
|
||||
RNPermissions: 34d678157c800b25b22a488e4d8babb57456e796
|
||||
RNLocalize: 414c99a0f6625a58dfee7f5780678d98c4ffabc2
|
||||
RNPermissions: 7ad8d93b004e6bc85c89e27a102afcde90e07efa
|
||||
RNReanimated: e42de406edd11350af29016cf6802ef16ee364d0
|
||||
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
|
||||
RNVectorIcons: 7923e585eaeb139b9f4531d25a125a1500162a0b
|
||||
VisionCamera: c1c171fcdbf18c438987847f785829c5638f3a4c
|
||||
VisionCamera: 420cba086c205b4dc102c3ad1158f72e41ad36c2
|
||||
Yoga: 99652481fcd320aefa4a7ef90095b95acd181952
|
||||
|
||||
PODFILE CHECKSUM: 51a6b3d8767f92f6783584e73a5acbcb4074e65d
|
||||
|
||||
2063
package-lock.json
generated
2063
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -14,8 +14,10 @@
|
||||
"translate": "node src/i18n/i18ncli.js build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.17.7",
|
||||
"@babel/eslint-parser": "^7.18.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@gorhom/bottom-sheet": "^4.3.1",
|
||||
"@react-native-async-storage/async-storage": "^1.17.7",
|
||||
"@react-native-community/cameraroll": "^4.1.2",
|
||||
"@react-native-community/checkbox": "^0.5.12",
|
||||
"@react-native-community/datetimepicker": "^6.1.0",
|
||||
@@ -37,6 +39,7 @@
|
||||
"i18next-resources-to-backend": "^1.0.0",
|
||||
"inaturalistjs": "github:inaturalist/inaturalistjs",
|
||||
"intl-pluralrules": "^1.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"radio-buttons-react-native": "^1.0.4",
|
||||
"react": "17.0.2",
|
||||
"react-i18next": "^11.16.1",
|
||||
@@ -77,19 +80,20 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@react-native-community/eslint-config": "^3.0.3",
|
||||
"@testing-library/react-native": "^9.0.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-flowtype": "^7.0.0",
|
||||
"eslint-plugin-i18next": "^6.0.0-2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^26.1.3",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-native": "^4.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"factoria": "^3.2.2",
|
||||
"faker": "^5.5.3",
|
||||
"flow-bin": "^0.162.0",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { getVersion, getBuildNumber } from "react-native-device-info";
|
||||
|
||||
import type { Node } from "react";
|
||||
import ViewWithFooter from "./SharedComponents/ViewWithFooter";
|
||||
import React from "react";
|
||||
import { getBuildNumber, getVersion } from "react-native-device-info";
|
||||
|
||||
import PlaceholderText from "./PlaceholderText";
|
||||
import ViewWithFooter from "./SharedComponents/ViewWithFooter";
|
||||
|
||||
const AboutScreen = ( ): Node => {
|
||||
const appVersion = getVersion( );
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { View, Pressable } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as React from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Avatar } from "react-native-paper";
|
||||
|
||||
import { textStyles, viewStyles } from "../../styles/sharedComponents/modal";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import { textStyles, viewStyles } from "../../styles/sharedComponents/modal";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
|
||||
type Props = {
|
||||
@@ -50,29 +50,21 @@ const CameraOptionsModal = ( { closeModal }: Props ): React.Node => {
|
||||
<TranslatedText text="Record-a-sound" />
|
||||
<TranslatedText text="Submit-without-evidence" />
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={navToStandardCamera}
|
||||
>
|
||||
<Pressable onPress={navToStandardCamera}>
|
||||
<Avatar.Icon size={40} icon="camera" />
|
||||
</Pressable>
|
||||
{!currentObs && (
|
||||
<Pressable
|
||||
onPress={navToPhotoGallery}
|
||||
>
|
||||
<Pressable onPress={navToPhotoGallery}>
|
||||
<Avatar.Icon size={40} icon="folder-multiple-image" />
|
||||
</Pressable>
|
||||
)}
|
||||
{!hasSound && (
|
||||
<Pressable
|
||||
onPress={navToSoundRecorder}
|
||||
>
|
||||
<Pressable onPress={navToSoundRecorder}>
|
||||
<Avatar.Icon size={40} icon="microphone" />
|
||||
</Pressable>
|
||||
)}
|
||||
{!currentObs && (
|
||||
<Pressable
|
||||
onPress={navToObsEdit}
|
||||
>
|
||||
<Pressable onPress={navToObsEdit}>
|
||||
<Avatar.Icon size={40} icon="square-edit-outline" />
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// @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/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Animated, StyleSheet } from "react-native";
|
||||
import { PinchGestureHandler, TapGestureHandler } from "react-native-gesture-handler";
|
||||
import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from "react-native-reanimated";
|
||||
import Reanimated, {
|
||||
Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue
|
||||
} from "react-native-reanimated";
|
||||
import { Camera } from "react-native-vision-camera";
|
||||
|
||||
import FocusSquare from "./FocusSquare";
|
||||
import { useIsForeground } from "./hooks/useIsForeground";
|
||||
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
|
||||
@@ -43,11 +45,13 @@ const CameraView = ( { camera, device }: Props ): Node => {
|
||||
zoom: z
|
||||
};
|
||||
}, [maxZoom, minZoom, zoom] );
|
||||
//#endregion
|
||||
// #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)
|
||||
// #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;
|
||||
@@ -55,13 +59,23 @@ const CameraView = ( { camera, device }: Props ): Node => {
|
||||
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 );
|
||||
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
|
||||
// #endregion
|
||||
|
||||
//#region Effects
|
||||
// #region Effects
|
||||
const neutralZoom = device?.neutralZoom ?? 1;
|
||||
useEffect( ( ) => {
|
||||
// Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { View, Animated } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { Animated, View } from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/camera/standardCamera";
|
||||
|
||||
@@ -35,7 +35,8 @@ const FocusSquare = ( { tappedCoordinates, tapToFocusAnimation }: Props ): Node
|
||||
left: tappedCoordinates.x,
|
||||
top: tappedCoordinates.y
|
||||
}
|
||||
]} />
|
||||
]}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { Portal, Modal } from "react-native-paper";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text } from "react-native";
|
||||
import { Modal, Portal } from "react-native-paper";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/camera/standardCamera";
|
||||
import PhotoCarousel from "../SharedComponents/PhotoCarousel";
|
||||
import { textStyles, viewStyles } from "../../styles/camera/standardCamera";
|
||||
import MediaViewer from "../MediaViewer/MediaViewer";
|
||||
import DeletePhotoDialog from "../SharedComponents/DeletePhotoDialog";
|
||||
import PhotoCarousel from "../SharedComponents/PhotoCarousel";
|
||||
|
||||
type Props = {
|
||||
photoUris: Array<string>,
|
||||
@@ -26,7 +26,7 @@ const PhotoPreview = ( { photoUris, setPhotoUris }: Props ): Node => {
|
||||
const showModal = ( ) => setMediaViewerVisible( true );
|
||||
const hideModal = ( ) => setMediaViewerVisible( false );
|
||||
|
||||
const handleSelection = ( photoUri ) => {
|
||||
const handleSelection = photoUri => {
|
||||
setInitialPhotoSelected( photoUri );
|
||||
showModal( );
|
||||
};
|
||||
@@ -38,7 +38,7 @@ const PhotoPreview = ( { photoUris, setPhotoUris }: Props ): Node => {
|
||||
setDeleteDialogVisible( false );
|
||||
};
|
||||
|
||||
const handleDelete = ( photoUri ) => {
|
||||
const handleDelete = photoUri => {
|
||||
setPhotoUriToDelete( photoUri );
|
||||
showDialog( );
|
||||
};
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import React, { useRef, useState, useContext, useEffect } from "react";
|
||||
import { Text, View, Pressable } from "react-native";
|
||||
import { Camera, useCameraDevices } from "react-native-vision-camera";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { Avatar, useTheme } from "react-native-paper";
|
||||
import Realm from "realm";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useContext, useEffect, useRef, useState
|
||||
} from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { Avatar, useTheme } from "react-native-paper";
|
||||
import { Camera, useCameraDevices } from "react-native-vision-camera";
|
||||
import Realm from "realm";
|
||||
|
||||
import { viewStyles } from "../../styles/camera/standardCamera";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import CameraView from "./CameraView";
|
||||
import PhotoPreview from "./PhotoPreview";
|
||||
import { textStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import realmConfig from "../../models/index";
|
||||
import Photo from "../../models/Photo";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import { viewStyles } from "../../styles/camera/standardCamera";
|
||||
import { textStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import CameraView from "./CameraView";
|
||||
import PhotoPreview from "./PhotoPreview";
|
||||
|
||||
const StandardCamera = ( ): Node => {
|
||||
const { colors } = useTheme( );
|
||||
@@ -72,7 +74,9 @@ const StandardCamera = ( ): Node => {
|
||||
}
|
||||
}, [photos] );
|
||||
|
||||
const renderCameraButton = ( icon ) => <Avatar.Icon size={40} icon={icon} style={{ backgroundColor: colors.background }} />;
|
||||
const renderCameraButton = icon => (
|
||||
<Avatar.Icon size={40} icon={icon} style={{ backgroundColor: colors.background }} />
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={viewStyles.container}>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AppState, AppStateStatus } from "react-native";
|
||||
|
||||
export const useIsForeground = ( ): boolean => {
|
||||
export default ( ): boolean => {
|
||||
const [isForeground, setIsForeground] = useState( true );
|
||||
|
||||
useEffect( ( ) => {
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
DrawerContentScrollView,
|
||||
DrawerItem
|
||||
} from "@react-navigation/drawer";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
props: any
|
||||
state: any,
|
||||
navigation: any,
|
||||
descriptors: any
|
||||
}
|
||||
|
||||
const CustomDrawerContent = ( { ...props }: Props ): Node => {
|
||||
// $FlowFixMe
|
||||
const { navigation } = props;
|
||||
const { state, navigation, descriptors } = props;
|
||||
|
||||
return (
|
||||
<DrawerContentScrollView {...props}>
|
||||
<DrawerContentScrollView state={state} navigation={navigation} descriptors={descriptors}>
|
||||
<DrawerItem
|
||||
label="identify"
|
||||
onPress={( ) => navigation.navigate( "identify" )}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/explore/explore";
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import { viewStyles } from "../../styles/explore/explore";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import FiltersIcon from "./FiltersIcon";
|
||||
|
||||
const Explore = ( ): Node => {
|
||||
@@ -19,14 +19,14 @@ const Explore = ( ): Node => {
|
||||
location,
|
||||
setLocation
|
||||
} = useContext( ExploreContext );
|
||||
const setTaxonId = ( getValue ) => {
|
||||
const setTaxonId = getValue => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
taxon_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const setPlaceId = ( getValue ) => {
|
||||
const setPlaceId = getValue => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
place_id: getValue( )
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// flow
|
||||
|
||||
import React from "react";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Image } from "react-native";
|
||||
// TODO: we'll probably need a custom dropdown picker which looks like a search bar
|
||||
// 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";
|
||||
@@ -41,54 +41,51 @@ const DropdownPicker = ( {
|
||||
}: Props ): Node => {
|
||||
const searchResults = useRemoteSearchResults( searchQuery, sources );
|
||||
|
||||
const placesItem = place => {
|
||||
return {
|
||||
label: place.name,
|
||||
value: place.uuid
|
||||
};
|
||||
};
|
||||
const placesItem = place => ( {
|
||||
label: place.name,
|
||||
value: place.uuid
|
||||
} );
|
||||
|
||||
const taxonIcon = taxa => <Image source={{ uri: taxa.default_photo.url }} style={imageStyles.pickerIcon} />;
|
||||
const userIcon = user => <Image source={{ uri: user.icon }} style={imageStyles.circularPickerIcon} />;
|
||||
const projectIcon = project => <Image source={{ uri: project.icon }} style={imageStyles.pickerIcon} />;
|
||||
const taxonIcon = taxa => (
|
||||
<Image source={{ uri: taxa.default_photo.url }} style={imageStyles.pickerIcon} />
|
||||
);
|
||||
const userIcon = user => (
|
||||
<Image source={{ uri: user.icon }} style={imageStyles.circularPickerIcon} />
|
||||
);
|
||||
const projectIcon = project => (
|
||||
<Image source={{ uri: project.icon }} style={imageStyles.pickerIcon} />
|
||||
);
|
||||
|
||||
const taxonItem = taxa => {
|
||||
// console.log( taxa, "taxa in item" );
|
||||
return {
|
||||
// TODO: match styling on the web; only show matched_term if the common name isn't clearly
|
||||
// linked to the search result
|
||||
label: `${taxa.preferred_common_name} (${taxa.matched_term})`,
|
||||
value: taxa.id,
|
||||
icon: taxonIcon( taxa )
|
||||
};
|
||||
};
|
||||
const taxonItem = taxa => ( {
|
||||
// TODO: match styling on the web; only show matched_term if the common name isn't clearly
|
||||
// linked to the search result
|
||||
label: `${taxa.preferred_common_name} (${taxa.matched_term})`,
|
||||
value: taxa.id,
|
||||
icon: taxonIcon( taxa )
|
||||
} );
|
||||
const userItem = user => ( {
|
||||
label: user.login,
|
||||
value: user.id,
|
||||
icon: userIcon( user )
|
||||
} );
|
||||
|
||||
const userItem = user => {
|
||||
return {
|
||||
label: user.login,
|
||||
value: user.id,
|
||||
icon: userIcon( user )
|
||||
};
|
||||
};
|
||||
|
||||
const projectItem = project => {
|
||||
return {
|
||||
label: project.title,
|
||||
value: project.id,
|
||||
icon: projectIcon( project )
|
||||
};
|
||||
};
|
||||
const projectItem = project => ( {
|
||||
label: project.title,
|
||||
value: project.id,
|
||||
icon: projectIcon( project )
|
||||
} );
|
||||
|
||||
const displayItems = ( ) => {
|
||||
if ( sources === "places" ) {
|
||||
return searchResults.map( item => placesItem( item ) );
|
||||
} else if ( sources === "taxa" ) {
|
||||
} if ( sources === "taxa" ) {
|
||||
return searchResults.map( item => taxonItem( item ) );
|
||||
} else if ( sources === "users" ) {
|
||||
} if ( sources === "users" ) {
|
||||
return searchResults.map( item => userItem( item ) );
|
||||
} else if ( sources === "projects" ) {
|
||||
} if ( sources === "projects" ) {
|
||||
return searchResults.map( item => projectItem( item ) );
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -101,8 +98,8 @@ const DropdownPicker = ( {
|
||||
value={value}
|
||||
items={displayItems( )}
|
||||
setValue={setValue}
|
||||
searchable={true}
|
||||
disableLocalSearch={true}
|
||||
searchable
|
||||
disableLocalSearch
|
||||
onChangeSearchText={setSearchQuery}
|
||||
placeholder={t( placeholder )}
|
||||
style={viewStyles.dropdown}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { Dimensions } from "react-native";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import ObservationViews from "../SharedComponents/ObservationViews/ObservationViews";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import BottomCard from "./BottomCard";
|
||||
|
||||
const { height } = Dimensions.get( "screen" );
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useContext } from "react";
|
||||
import CheckBox from "@react-native-community/checkbox";
|
||||
import { t } from "i18next";
|
||||
import RadioButtonRN from "radio-buttons-react-native";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
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 { pickerSelectStyles, viewStyles } from "../../styles/explore/exploreFilters";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import TaxonLocationSearch from "./TaxonLocationSearch";
|
||||
import { pickerSelectStyles, viewStyles } from "../../styles/explore/exploreFilters";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import ScrollNoFooter from "../SharedComponents/ScrollNoFooter";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import ExploreFooter from "./ExploreFooter";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import ResetFiltersButton from "./ResetFiltersButton";
|
||||
import TaxonLocationSearch from "./TaxonLocationSearch";
|
||||
|
||||
const ExploreFilters = ( ): Node => {
|
||||
const [project, setProject] = useState( "" );
|
||||
@@ -28,14 +28,14 @@ const ExploreFilters = ( ): Node => {
|
||||
setUnappliedFilters
|
||||
} = useContext( ExploreContext );
|
||||
|
||||
const setProjectId = ( getValue ) => {
|
||||
const setProjectId = getValue => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
project_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const setUserId = ( getValue ) => {
|
||||
const setUserId = getValue => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
user_id: getValue( )
|
||||
@@ -97,7 +97,7 @@ const ExploreFilters = ( ): Node => {
|
||||
{ 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-phylum" ), value: "phylum" },
|
||||
{ label: t( "Ranks-subphylum" ), value: "subphylum" },
|
||||
{ label: t( "Ranks-superclass" ), value: "superclass" },
|
||||
{ label: t( "Ranks-class" ), value: "class" },
|
||||
@@ -135,7 +135,7 @@ const ExploreFilters = ( ): Node => {
|
||||
const projectId = unappliedFilters ? unappliedFilters.project_id : null;
|
||||
const userId = unappliedFilters ? unappliedFilters.user_id : null;
|
||||
|
||||
const renderQualityGradeCheckbox = ( qualityGrade ) => {
|
||||
const renderQualityGradeCheckbox = qualityGrade => {
|
||||
const filter = unappliedFilters.quality_grade;
|
||||
const hasFilter = filter.includes( qualityGrade );
|
||||
|
||||
@@ -163,7 +163,7 @@ const ExploreFilters = ( ): Node => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderMediaCheckbox = ( mediaType ) => {
|
||||
const renderMediaCheckbox = mediaType => {
|
||||
const { sounds, photos } = unappliedFilters;
|
||||
return (
|
||||
<CheckBox
|
||||
@@ -188,8 +188,10 @@ const ExploreFilters = ( ): Node => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatusCheckbox = ( status ) => {
|
||||
const { native, captive, introduced, threatened } = unappliedFilters;
|
||||
const renderStatusCheckbox = status => {
|
||||
const {
|
||||
native, captive, introduced, threatened
|
||||
} = unappliedFilters;
|
||||
|
||||
let value;
|
||||
|
||||
@@ -220,9 +222,9 @@ const ExploreFilters = ( ): Node => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderRankPicker = ( rank ) => (
|
||||
const renderRankPicker = rank => (
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) => {
|
||||
onValueChange={itemValue => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
// $FlowFixMe
|
||||
@@ -242,7 +244,7 @@ const ExploreFilters = ( ): Node => {
|
||||
|
||||
const includesMonth = value => unappliedFilters.months.includes( value );
|
||||
|
||||
const fillInMonths = ( itemValue ) => {
|
||||
const fillInMonths = itemValue => {
|
||||
months.forEach( ( { value } ) => {
|
||||
if ( value >= firstMonth && value <= itemValue && !includesMonth( value ) ) {
|
||||
unappliedFilters.months.push( value );
|
||||
@@ -257,7 +259,7 @@ const ExploreFilters = ( ): Node => {
|
||||
return (
|
||||
<>
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) => {
|
||||
onValueChange={itemValue => {
|
||||
unappliedFilters.months = [itemValue];
|
||||
setUnappliedFilters( { ...unappliedFilters } );
|
||||
}}
|
||||
@@ -267,7 +269,7 @@ const ExploreFilters = ( ): Node => {
|
||||
value={firstMonth}
|
||||
/>
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) => fillInMonths( itemValue )}
|
||||
onValueChange={itemValue => fillInMonths( itemValue )}
|
||||
items={months}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
@@ -287,20 +289,20 @@ const ExploreFilters = ( ): Node => {
|
||||
initial={1}
|
||||
boxStyle={viewStyles.radioButtonBox}
|
||||
selectedBtn={( { type } ) => {
|
||||
if ( type === "desc" || type === "asc" ) {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
order: type,
|
||||
order_by: "created_at"
|
||||
} );
|
||||
} else {
|
||||
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
|
||||
} );
|
||||
}
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
order: "desc",
|
||||
order_by: type
|
||||
} );
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<View style={viewStyles.filtersRow}>
|
||||
@@ -396,7 +398,7 @@ const ExploreFilters = ( ): Node => {
|
||||
/>
|
||||
<TranslatedText text="Photo-Licensing" />
|
||||
<RNPickerSelect
|
||||
onValueChange={( itemValue ) => {
|
||||
onValueChange={itemValue => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
photo_license: itemValue === "all" ? [] : [itemValue]
|
||||
@@ -405,11 +407,15 @@ const ExploreFilters = ( ): Node => {
|
||||
items={photoLicenses}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={pickerSelectStyles}
|
||||
value={unappliedFilters.photo_license.length > 0 ? unappliedFilters.photo_license[0] : "all"}
|
||||
value={
|
||||
unappliedFilters.photo_license.length > 0
|
||||
? unappliedFilters.photo_license[0]
|
||||
: "all"
|
||||
}
|
||||
/>
|
||||
<TranslatedText text="Description-Tags" />
|
||||
<InputField
|
||||
handleTextChange={( q ) => {
|
||||
handleTextChange={q => {
|
||||
setUnappliedFilters( {
|
||||
...unappliedFilters,
|
||||
q
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @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 type { Node } from "react";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import { viewStyles } from "../../styles/explore/exploreFilters";
|
||||
@@ -26,7 +26,7 @@ const ExploreFooter = ( ): Node => {
|
||||
|
||||
return (
|
||||
<View style={viewStyles.footer}>
|
||||
<HeaderBackButton onPress={clearFiltersAndNavigate} style={viewStyles.element}/>
|
||||
<HeaderBackButton onPress={clearFiltersAndNavigate} style={viewStyles.element} />
|
||||
<RoundGreenButton
|
||||
handlePress={applyFiltersAndNavigate}
|
||||
buttonText="Apply Filters"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import type { Node } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { View } from "react-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 { textStyles, viewStyles } from "../../styles/explore/explore";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import FiltersIcon from "./FiltersIcon";
|
||||
import TaxonLocationSearch from "./TaxonLocationSearch";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as React from "react";
|
||||
import { Pressable } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
import { viewStyles } from "../../styles/observations/messagesIcon";
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import * as React from "react";
|
||||
import { Pressable } from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/observations/messagesIcon";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import { viewStyles } from "../../styles/observations/messagesIcon";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
|
||||
const ResetFiltersButton = ( ): React.Node => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useState, useCallback } from "react";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useContext, useState } from "react";
|
||||
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
import { ExploreContext } from "../../providers/contexts";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import DropdownPicker from "./DropdownPicker";
|
||||
|
||||
const TaxonLocationSearch = ( ): Node => {
|
||||
const {
|
||||
@@ -35,14 +35,14 @@ const TaxonLocationSearch = ( ): Node => {
|
||||
setTaxonOpen( false );
|
||||
}, [] );
|
||||
|
||||
const setTaxonId = ( getValue ) => {
|
||||
const setTaxonId = getValue => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
taxon_id: getValue( )
|
||||
} );
|
||||
};
|
||||
|
||||
const setPlaceId = ( getValue ) => {
|
||||
const setPlaceId = getValue => {
|
||||
setExploreFilters( {
|
||||
...exploreFilters,
|
||||
place_id: getValue( )
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Text, View, Image } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Image, Text, View } from "react-native";
|
||||
import TinderCard from "react-tinder-card";
|
||||
|
||||
import { viewStyles, imageStyles, textStyles } from "../../styles/identify/identify";
|
||||
import Observation from "../../models/Observation";
|
||||
import markAsReviewed from "./helpers/markAsReviewed";
|
||||
import createIdentification from "./helpers/createIdentification";
|
||||
import { imageStyles, textStyles, viewStyles } from "../../styles/identify/identify";
|
||||
import PlaceholderText from "../PlaceholderText";
|
||||
import createIdentification from "./helpers/createIdentification";
|
||||
import markAsReviewed from "./helpers/markAsReviewed";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
observationList: Array<Object>,
|
||||
testID: string
|
||||
}
|
||||
|
||||
const CardSwipeView = ( { loading, observationList, testID }: Props ): Node => {
|
||||
const CardSwipeView = ( { observationList }: Props ): Node => {
|
||||
const [totalSwiped, setTotalSwiped] = useState( 0 );
|
||||
const onSwipe = async ( direction, id, isSpecies, agreeParams ) => {
|
||||
if ( direction === "left" ) {
|
||||
@@ -26,7 +24,7 @@ const CardSwipeView = ( { loading, observationList, testID }: Props ): Node => {
|
||||
const agreed = await createIdentification( agreeParams );
|
||||
console.log( agreed, "agreed in card swipe" );
|
||||
}
|
||||
console.log( "You swiped: " + direction );
|
||||
console.log( `You swiped: ${direction}` );
|
||||
};
|
||||
|
||||
const onCardLeftScreen = ( ) => {
|
||||
@@ -56,7 +54,7 @@ const CardSwipeView = ( { loading, observationList, testID }: Props ): Node => {
|
||||
return (
|
||||
<TinderCard
|
||||
key={obs.id}
|
||||
onSwipe={( dir ) => onSwipe( dir, obs.uuid, isSpecies, agreeParams )}
|
||||
onSwipe={dir => onSwipe( dir, obs.uuid, isSpecies, agreeParams )}
|
||||
onCardLeftScreen={onCardLeftScreen}
|
||||
preventSwipe={preventSwipeDirections}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Pressable, Text, Image, View, ActivityIndicator } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ActivityIndicator, Image, Pressable, Text, View
|
||||
} from "react-native";
|
||||
|
||||
import Observation from "../../models/Observation";
|
||||
import { textStyles, imageStyles, viewStyles } from "../../styles/sharedComponents/observationViews/gridItem";
|
||||
import {
|
||||
imageStyles,
|
||||
textStyles,
|
||||
viewStyles
|
||||
} from "../../styles/sharedComponents/observationViews/gridItem";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import createIdentification from "./helpers/createIdentification";
|
||||
|
||||
@@ -16,7 +22,9 @@ type Props = {
|
||||
setReviewedIds: Function
|
||||
}
|
||||
|
||||
const GridItem = ( { item, handlePress, reviewedIds, setReviewedIds }: Props ): Node => {
|
||||
const GridItem = ( {
|
||||
item, handlePress, reviewedIds, setReviewedIds
|
||||
}: Props ): Node => {
|
||||
const [showLoadingWheel, setShowLoadingWheel] = useState( false );
|
||||
const commonName = item.taxon && item.taxon.preferred_common_name;
|
||||
const name = item.taxon ? item.taxon.name : "unknown";
|
||||
@@ -31,7 +39,10 @@ const GridItem = ( { item, handlePress, reviewedIds, setReviewedIds }: Props ):
|
||||
|
||||
const agreeWithObservation = async ( ) => {
|
||||
setShowLoadingWheel( true );
|
||||
const results = await createIdentification( { observation_id: item.uuid, taxon_id: item.taxon.id } );
|
||||
const results = await createIdentification( {
|
||||
observation_id: item.uuid,
|
||||
taxon_id: item.taxon.id
|
||||
} );
|
||||
if ( results === 1 ) {
|
||||
const ids = Array.from( reviewedIds );
|
||||
ids.push( item.id );
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { FlatList, ActivityIndicator } from "react-native";
|
||||
// import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { ActivityIndicator, FlatList } from "react-native";
|
||||
|
||||
import GridItem from "./GridItem";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
observationList: Array<Object>,
|
||||
testID: string,
|
||||
taxonId?: number
|
||||
testID: string
|
||||
}
|
||||
|
||||
const GridView = ( {
|
||||
loading,
|
||||
observationList,
|
||||
testID,
|
||||
taxonId
|
||||
testID
|
||||
}: Props ): Node => {
|
||||
const [reviewedIds, setReviewedIds] = useState( [] );
|
||||
// const navigation = useNavigation( );
|
||||
// const { name } = useRoute( );
|
||||
|
||||
// const navToObsDetails = observation => navigation.navigate( "ObsDetails", { uuid: observation.uuid } );
|
||||
|
||||
const renderGridItem = ( { item } ) => (
|
||||
<GridItem
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import type { Node } from "react";
|
||||
import { Text, View, Pressable } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import useObservations from "./hooks/useObservations";
|
||||
import GridView from "./GridView";
|
||||
import { viewStyles } from "../../styles/identify/identify";
|
||||
import DropdownPicker from "../Explore/DropdownPicker";
|
||||
import { viewStyles } from "./../../styles/identify/identify";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import CardSwipeView from "./CardSwipeView";
|
||||
import GridView from "./GridView";
|
||||
import useObservations from "./hooks/useObservations";
|
||||
|
||||
const Identify = ( ): Node => {
|
||||
const [view, setView] = React.useState( "grid" );
|
||||
@@ -20,8 +20,8 @@ const Identify = ( ): Node => {
|
||||
const [taxonId, setTaxonId] = useState( null );
|
||||
const { observations, loading } = useObservations( placeId, taxonId );
|
||||
|
||||
const updatePlaceId = ( getValue ) => setPlaceId( getValue( ) );
|
||||
const updateTaxonId = ( getValue ) => setTaxonId( getValue( ) );
|
||||
const updatePlaceId = getValue => setPlaceId( getValue( ) );
|
||||
const updateTaxonId = getValue => setTaxonId( getValue( ) );
|
||||
|
||||
const setGridView = ( ) => setView( "grid" );
|
||||
const setCardView = ( ) => setView( "card" );
|
||||
@@ -29,21 +29,16 @@ const Identify = ( ): Node => {
|
||||
const renderView = ( ) => {
|
||||
if ( view === "card" ) {
|
||||
return (
|
||||
<CardSwipeView
|
||||
loading={loading}
|
||||
observationList={observations}
|
||||
testID="Identify.cardView"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<GridView
|
||||
loading={loading}
|
||||
observationList={observations}
|
||||
testID="Identify.observationGrid"
|
||||
/>
|
||||
<CardSwipeView observationList={observations} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<GridView
|
||||
loading={loading}
|
||||
observationList={observations}
|
||||
testID="Identify.observationGrid"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const { t } = useTranslation( );
|
||||
@@ -51,7 +46,7 @@ const Identify = ( ): Node => {
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<View style={viewStyles.toggleViewRow}>
|
||||
<Pressable
|
||||
<Pressable
|
||||
onPress={setCardView}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
@@ -64,7 +59,7 @@ const Identify = ( ): Node => {
|
||||
>
|
||||
<Text>{ t( "Grid-View" ) }</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
<DropdownPicker
|
||||
searchQuery={location}
|
||||
setSearchQuery={setLocation}
|
||||
@@ -79,7 +74,7 @@ const Identify = ( ): Node => {
|
||||
sources="taxa"
|
||||
value={taxonId}
|
||||
/>
|
||||
{renderView( )}
|
||||
{renderView( )}
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import Observation from "../../../models/Observation";
|
||||
|
||||
|
||||
const useObservations = ( placeId: ?string, taxonId: ?number ): {
|
||||
observations: Array<Object>,
|
||||
loading: boolean
|
||||
@@ -39,14 +38,14 @@ const useObservations = ( placeId: ?string, taxonId: ?number ): {
|
||||
params.fields.observation_photos.photo.medium_url = true;
|
||||
|
||||
const response = await inatjs.observations.search( params );
|
||||
const results = response.results;
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setLoading( false );
|
||||
setObservations( results );
|
||||
} catch ( e ) {
|
||||
setLoading( false );
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch observations for identify:", JSON.stringify( e.response ), );
|
||||
console.log( "Couldn't fetch observations for identify:", JSON.stringify( e.response ) );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,58 +1,68 @@
|
||||
// @flow
|
||||
import { create } from "apisauce";
|
||||
import { Platform } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import SInfo from "react-native-sensitive-info";
|
||||
import {
|
||||
getBuildNumber, getDeviceType, getSystemName, getSystemVersion, getVersion
|
||||
} from "react-native-device-info";
|
||||
import jwt from "react-native-jwt-io";
|
||||
import * as RNLocalize from "react-native-localize";
|
||||
import RNSInfo from "react-native-sensitive-info";
|
||||
import jwt from "react-native-jwt-io";
|
||||
import {Platform} from "react-native";
|
||||
import {getBuildNumber, getDeviceType, getSystemName, getSystemVersion, getVersion} from "react-native-device-info";
|
||||
import Realm from "realm";
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import realmConfig from "../../models/index";
|
||||
|
||||
// Base API domain can be overridden (in case we want to use staging URL) - either by placing it in .env file, or
|
||||
// in an environment variable.
|
||||
// Base API domain can be overridden (in case we want to use staging URL) -
|
||||
// either by placing it in .env file, or in an environment variable.
|
||||
const API_HOST: string = Config.OAUTH_API_URL || process.env.OAUTH_API_URL || "https://www.inaturalist.org";
|
||||
|
||||
// User agent being used, when calling the iNat APIs
|
||||
// eslint-disable-next-line max-len
|
||||
const USER_AGENT = `iNaturalistRN/${getVersion()} ${getDeviceType()} (Build ${getBuildNumber()}) ${getSystemName()}/${getSystemVersion()}`;
|
||||
|
||||
const JWT_TOKEN_EXPIRATION_MINS = 25; // JWT Tokens expire after 30 mins - consider 25 mins as the max time (safe margin)
|
||||
// JWT Tokens expire after 30 mins - consider 25 mins as the max time (safe margin)
|
||||
const JWT_TOKEN_EXPIRATION_MINS = 25;
|
||||
|
||||
/**
|
||||
* Creates base API client for all requests
|
||||
* @param additionalHeaders any additional headers that will be passed to the API
|
||||
*/
|
||||
const createAPI = ( additionalHeaders: any ) => {
|
||||
return create( {
|
||||
baseURL: API_HOST,
|
||||
headers: { "User-Agent": USER_AGENT, ...additionalHeaders }
|
||||
} );
|
||||
};
|
||||
const createAPI = ( additionalHeaders: any ) => create( {
|
||||
baseURL: API_HOST,
|
||||
headers: { "User-Agent": USER_AGENT, ...additionalHeaders }
|
||||
} );
|
||||
|
||||
/**
|
||||
* Returns the API access token to be used with all iNaturalist API calls
|
||||
* Returns whether we're currently logged in.
|
||||
*
|
||||
* @param useJWT if true, we'll use JSON Web Token instead of the "regular" access token
|
||||
* @param allowAnonymousJWTToken (optional=false) if true and user is not logged-in, use anonymous JWT
|
||||
* @returns {Promise<string|*>} access token, null if not logged in
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const getAPIToken = async ( useJWT: boolean = false, allowAnonymousJWTToken: boolean = false ): Promise<?string> => {
|
||||
let loggedIn = await isLoggedIn();
|
||||
if ( !loggedIn ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( useJWT ) {
|
||||
return getJWTToken( allowAnonymousJWTToken );
|
||||
} else {
|
||||
const accessToken = await RNSInfo.getItem( "accessToken", {} );
|
||||
return `Bearer ${accessToken}`;
|
||||
}
|
||||
const isLoggedIn = async (): Promise<boolean> => {
|
||||
const accessToken = await RNSInfo.getItem( "accessToken", {} );
|
||||
return typeof accessToken === "string";
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the access token to be used in case of an anonymous JWT (e.g. used when getting taxon suggestions)
|
||||
* Signs out the user
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const signOut = async (
|
||||
options: { deleteRealm: boolean }
|
||||
) => {
|
||||
if ( options?.deleteRealm ) {
|
||||
Realm.deleteFile( realmConfig );
|
||||
}
|
||||
await RNSInfo.deleteItem( "jwtToken", {} );
|
||||
await RNSInfo.deleteItem( "jwtTokenExpiration", {} );
|
||||
await RNSInfo.deleteItem( "username", {} );
|
||||
await RNSInfo.deleteItem( "accessToken", {} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the access token to be used in case of an anonymous JWT (e.g. used
|
||||
* when getting taxon suggestions)
|
||||
* @returns encoded anonymous JWT
|
||||
*/
|
||||
const getAnonymousJWTToken = () => {
|
||||
@@ -67,7 +77,8 @@ const getAnonymousJWTToken = () => {
|
||||
/**
|
||||
* Returns most recent JWT (JSON Web Token) for API authentication - renews the token if necessary
|
||||
*
|
||||
* @param allowAnonymousJWTToken (optional=false) if true and user is not logged-in, use anonymous JWT
|
||||
* @param allowAnonymousJWTToken (optional=false) if true and user is not
|
||||
* logged-in, use anonymous JWT
|
||||
* @returns {Promise<string|*>}
|
||||
*/
|
||||
const getJWTToken = async ( allowAnonymousJWTToken: boolean = false ): Promise<?string> => {
|
||||
@@ -77,7 +88,7 @@ const getJWTToken = async ( allowAnonymousJWTToken: boolean = false ): Promise<?
|
||||
jwtTokenExpiration = parseInt( jwtTokenExpiration, 10 );
|
||||
}
|
||||
|
||||
let loggedIn = await isLoggedIn();
|
||||
const loggedIn = await isLoggedIn();
|
||||
|
||||
if ( !loggedIn && allowAnonymousJWTToken ) {
|
||||
// User not logged in, and anonymous JWT is allowed - return it
|
||||
@@ -89,18 +100,20 @@ const getJWTToken = async ( allowAnonymousJWTToken: boolean = false ): Promise<?
|
||||
}
|
||||
|
||||
if (
|
||||
!jwtToken ||
|
||||
( Date.now() - jwtTokenExpiration ) / 1000 > JWT_TOKEN_EXPIRATION_MINS * 60
|
||||
!jwtToken
|
||||
|| ( Date.now() - jwtTokenExpiration ) / 1000 > JWT_TOKEN_EXPIRATION_MINS * 60
|
||||
) {
|
||||
// JWT Tokens expire after 30 mins - if the token is non-existent or older than 25 mins (safe margin) - ask for a new one
|
||||
// JWT Tokens expire after 30 mins - if the token is non-existent or older
|
||||
// than 25 mins (safe margin) - ask for a new one
|
||||
|
||||
const accessToken = await RNSInfo.getItem( "accessToken", {} );
|
||||
const api = createAPI( { Authorization: `Bearer ${accessToken}` } );
|
||||
const response = await api.get( "/users/api_token.json" );
|
||||
|
||||
if ( !response.ok ) {
|
||||
// this deletes the user JWT and saved login details when a user is not actually signed in anymore
|
||||
// for example, if they installed, deleted, and reinstalled the app without logging out
|
||||
// this deletes the user JWT and saved login details when a user is not
|
||||
// actually signed in anymore for example, if they installed, deleted,
|
||||
// and reinstalled the app without logging out
|
||||
if ( response.status === 401 ) {
|
||||
signOut( { deleteRealm: true } );
|
||||
}
|
||||
@@ -114,105 +127,37 @@ const getJWTToken = async ( allowAnonymousJWTToken: boolean = false ): Promise<?
|
||||
jwtToken = response.data.api_token;
|
||||
jwtTokenExpiration = Date.now();
|
||||
|
||||
await SInfo.setItem( "jwtToken", jwtToken, {} );
|
||||
await SInfo.setItem( "jwtTokenExpiration", jwtTokenExpiration.toString(), {} );
|
||||
await RNSInfo.setItem( "jwtToken", jwtToken, {} );
|
||||
await RNSInfo.setItem( "jwtTokenExpiration", jwtTokenExpiration.toString(), {} );
|
||||
|
||||
return jwtToken;
|
||||
} else {
|
||||
// Current JWT token is still fresh/valid - return it as-is
|
||||
return jwtToken;
|
||||
}
|
||||
// Current JWT token is still fresh/valid - return it as-is
|
||||
return jwtToken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates a user and saves authentication details to secure storage, to be used when calling iNat APIs.
|
||||
* Returns the API access token to be used with all iNaturalist API calls
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
* @returns false in case of authentication error, true otherwise.
|
||||
* @param useJWT if true, we'll use JSON Web Token instead of the "regular" access token
|
||||
* @param allowAnonymousJWTToken (optional=false) if true and user is not
|
||||
* logged-in, use anonymous JWT
|
||||
* @returns {Promise<string|*>} access token, null if not logged in
|
||||
*/
|
||||
const authenticateUser = async (
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<boolean> => {
|
||||
const userDetails = await verifyCredentials( username, password );
|
||||
|
||||
if ( !userDetails ) {
|
||||
return false;
|
||||
const getAPIToken = async (
|
||||
useJWT: boolean = false,
|
||||
allowAnonymousJWTToken: boolean = false
|
||||
): Promise<?string> => {
|
||||
const loggedIn = await isLoggedIn();
|
||||
if ( !loggedIn ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { userId, username: remoteUsername, accessToken } = userDetails;
|
||||
if ( !userId ) {
|
||||
return false;
|
||||
if ( useJWT ) {
|
||||
return getJWTToken( allowAnonymousJWTToken );
|
||||
}
|
||||
|
||||
// Save authentication details to secure storage
|
||||
await SInfo.setItem( "username", remoteUsername, {} );
|
||||
await SInfo.setItem( "accessToken", accessToken, {} );
|
||||
// await SInfo.setItem( "userId", userId, {} );
|
||||
|
||||
// Save userId to local, encrypted storage
|
||||
const currentUser = { id: userId, login: remoteUsername, signedIn: true };
|
||||
const realm = await Realm.open( realmConfig );
|
||||
realm.write( ( ) => {
|
||||
realm?.create( "User", currentUser, "modified" );
|
||||
} );
|
||||
realm.close( );
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a new user
|
||||
*
|
||||
* @param email
|
||||
* @param username
|
||||
* @param password
|
||||
* @param license (optional)
|
||||
* @param time_zone (optional)
|
||||
*
|
||||
* @returns null if successful, otherwise an error string
|
||||
*/
|
||||
const registerUser = async (
|
||||
email: string,
|
||||
username: string,
|
||||
password: string,
|
||||
license: void | string,
|
||||
time_zone: void | string
|
||||
): any => {
|
||||
const formData = new FormData();
|
||||
formData.append( "username", username );
|
||||
formData.append( "user[email]", email );
|
||||
formData.append( "user[login]", username );
|
||||
formData.append( "user[password]", password );
|
||||
formData.append( "user[password_confirmation]", password );
|
||||
// TODO - support for iNat site_id
|
||||
if ( license ) {
|
||||
formData.append( "user[preferred_observation_license]", license );
|
||||
formData.append( "user[preferred_photo_license]", license );
|
||||
formData.append( "user[preferred_sound_license]", license );
|
||||
}
|
||||
const locales = RNLocalize.getLocales();
|
||||
|
||||
formData.append( "user[locale]", locales[0].languageCode );
|
||||
if ( time_zone ) {
|
||||
formData.append( "user[time_zone]", time_zone );
|
||||
}
|
||||
|
||||
const api = createAPI();
|
||||
let response = await api.post( "/users.json", formData );
|
||||
|
||||
if ( !response.ok ) {
|
||||
console.error(
|
||||
"registerUser failed when calling /users.json - ",
|
||||
response.problem,
|
||||
response.status
|
||||
);
|
||||
return response.data.errors[0];
|
||||
}
|
||||
|
||||
// console.info( "registerUser - success" );
|
||||
return null;
|
||||
const accessToken = await RNSInfo.getItem( "accessToken", {} );
|
||||
return `Bearer ${accessToken}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -220,7 +165,8 @@ const registerUser = async (
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
* @return null in case of error, otherwise an object of accessToken, username (=iNaturalist username)
|
||||
* @return null in case of error, otherwise an object of accessToken,
|
||||
* username (=iNaturalist username)
|
||||
*/
|
||||
const verifyCredentials = async (
|
||||
username: string,
|
||||
@@ -276,20 +222,98 @@ const verifyCredentials = async (
|
||||
// console.log( "verifyCredentials - logged in username ", iNatUsername );
|
||||
|
||||
return {
|
||||
accessToken: accessToken,
|
||||
accessToken,
|
||||
username: iNatUsername,
|
||||
userId: iNatID
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether we're currently logged in.
|
||||
* Authenticates a user and saves authentication details to secure storage, to
|
||||
* be used when calling iNat APIs.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* @param username
|
||||
* @param password
|
||||
* @returns false in case of authentication error, true otherwise.
|
||||
*/
|
||||
const isLoggedIn = async (): Promise<boolean> => {
|
||||
const accessToken = await RNSInfo.getItem( "accessToken", {} );
|
||||
return typeof accessToken === "string";
|
||||
const authenticateUser = async (
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<boolean> => {
|
||||
const userDetails = await verifyCredentials( username, password );
|
||||
|
||||
if ( !userDetails ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { userId, username: remoteUsername, accessToken } = userDetails;
|
||||
if ( !userId ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save authentication details to secure storage
|
||||
await RNSInfo.setItem( "username", remoteUsername, {} );
|
||||
await RNSInfo.setItem( "accessToken", accessToken, {} );
|
||||
// await SInfo.setItem( "userId", userId, {} );
|
||||
|
||||
// Save userId to local, encrypted storage
|
||||
const currentUser = { id: userId, login: remoteUsername, signedIn: true };
|
||||
const realm = await Realm.open( realmConfig );
|
||||
realm.write( ( ) => {
|
||||
realm?.create( "User", currentUser, "modified" );
|
||||
} );
|
||||
realm.close( );
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a new user
|
||||
*
|
||||
* @param email
|
||||
* @param username
|
||||
* @param password
|
||||
* @param license (optional)
|
||||
* @param time_zone (optional)
|
||||
*
|
||||
* @returns null if successful, otherwise an error string
|
||||
*/
|
||||
const registerUser = async (
|
||||
email: string,
|
||||
username: string,
|
||||
password: string,
|
||||
license: void | string
|
||||
): any => {
|
||||
const formData = new FormData();
|
||||
formData.append( "username", username );
|
||||
formData.append( "user[email]", email );
|
||||
formData.append( "user[login]", username );
|
||||
formData.append( "user[password]", password );
|
||||
formData.append( "user[password_confirmation]", password );
|
||||
// TODO - support for iNat site_id
|
||||
if ( license ) {
|
||||
formData.append( "user[preferred_observation_license]", license );
|
||||
formData.append( "user[preferred_photo_license]", license );
|
||||
formData.append( "user[preferred_sound_license]", license );
|
||||
}
|
||||
const locales = RNLocalize.getLocales();
|
||||
|
||||
formData.append( "user[locale]", locales[0].languageCode );
|
||||
|
||||
const api = createAPI();
|
||||
const response = await api.post( "/users.json", formData );
|
||||
|
||||
if ( !response.ok ) {
|
||||
console.error(
|
||||
"registerUser failed when calling /users.json - ",
|
||||
response.problem,
|
||||
response.status
|
||||
);
|
||||
return response.data.errors[0];
|
||||
}
|
||||
|
||||
// console.info( "registerUser - success" );
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -297,10 +321,7 @@ const isLoggedIn = async (): Promise<boolean> => {
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const getUsername = async (): Promise<string> => {
|
||||
return await RNSInfo.getItem( "username", {} );
|
||||
};
|
||||
|
||||
const getUsername = async (): Promise<string> => RNSInfo.getItem( "username", {} );
|
||||
|
||||
/**
|
||||
* Returns the logged-in user
|
||||
@@ -312,48 +333,30 @@ const getUser = async (): Promise<Object | null> => {
|
||||
return realm.objects( "User" ).filtered( "signedIn == true" )[0];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the logged-in userId
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const getUserId = async (): Promise<string | null> => {
|
||||
const getUserId = async (): Promise<string | null> => {
|
||||
const realm = await Realm.open( realmConfig );
|
||||
const currentUser = realm.objects( "User" ).filtered( "signedIn == true" )[0];
|
||||
const currentUserId = currentUser?.id?.toString( );
|
||||
// TODO: still need to figure out the right way/time to close realm
|
||||
// but omitting for now bc it interferes with a user being able to save a local observation if they're logged out
|
||||
// realm.close( );
|
||||
// TODO: still need to figure out the right way/time to close realm but
|
||||
// omitting for now bc it interferes with a user being able to save a local
|
||||
// observation if they're logged out realm.close( );
|
||||
return currentUserId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Signs out the user
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const signOut = async ( options: {
|
||||
deleteRealm: boolean
|
||||
} ) => {
|
||||
if ( options?.deleteRealm ) {
|
||||
Realm.deleteFile( realmConfig );
|
||||
}
|
||||
await SInfo.deleteItem( "jwtToken", {} );
|
||||
await SInfo.deleteItem( "jwtTokenExpiration", {} );
|
||||
await SInfo.deleteItem( "username", {} );
|
||||
await SInfo.deleteItem( "accessToken", {} );
|
||||
};
|
||||
|
||||
export {
|
||||
API_HOST,
|
||||
authenticateUser,
|
||||
registerUser,
|
||||
getAPIToken,
|
||||
isLoggedIn,
|
||||
getUsername,
|
||||
signOut,
|
||||
getJWTToken,
|
||||
getUser,
|
||||
getUserId
|
||||
getUserId,
|
||||
getUsername,
|
||||
isLoggedIn,
|
||||
registerUser,
|
||||
signOut
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
@@ -10,15 +13,20 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import {Button, Paragraph, Dialog, Portal, Text, TextInput} from "react-native-paper";
|
||||
import {
|
||||
Button, Dialog, Paragraph, Portal, Text, TextInput
|
||||
} from "react-native-paper";
|
||||
|
||||
import { textStyles, viewStyles, imageStyles } from "../../styles/login/login";
|
||||
import { isLoggedIn, authenticateUser, getUsername, getUserId, signOut } from "./AuthenticationService";
|
||||
import colors from "../../styles/colors";
|
||||
import { imageStyles, textStyles, viewStyles } from "../../styles/login/login";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {colors} from "../../styles/global";
|
||||
import {
|
||||
authenticateUser,
|
||||
getUserId,
|
||||
getUsername,
|
||||
isLoggedIn,
|
||||
signOut
|
||||
} from "./AuthenticationService";
|
||||
|
||||
const Login = ( ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
@@ -38,7 +46,7 @@ const Login = ( ): Node => {
|
||||
let isCurrent = true;
|
||||
|
||||
const fetchLoggedIn = async ( ) => {
|
||||
if ( !isCurrent ) {return;}
|
||||
if ( !isCurrent ) { return; }
|
||||
|
||||
setLoggedIn( await isLoggedIn( ) );
|
||||
if ( loggedIn ) {
|
||||
@@ -60,7 +68,6 @@ const Login = ( ): Node => {
|
||||
password
|
||||
);
|
||||
|
||||
|
||||
if ( !success ) {
|
||||
setError( t( "Invalid-login" ) );
|
||||
setLoading( false );
|
||||
@@ -119,7 +126,11 @@ const Login = ( ): Node => {
|
||||
|
||||
const loginForm = (
|
||||
<>
|
||||
<Image style={imageStyles.logo} resizeMode="contain" source={require( "../../images/inat_logo.png" )} />
|
||||
<Image
|
||||
style={imageStyles.logo}
|
||||
resizeMode="contain"
|
||||
source={require( "../../images/inat_logo.png" )}
|
||||
/>
|
||||
<Text style={textStyles.header}>{t( "Login-header" )}</Text>
|
||||
<Text style={textStyles.subtitle}>{t( "Login-sub-title" )}</Text>
|
||||
<Text style={textStyles.fieldText}>{t( "Username-or-Email" )}</Text>
|
||||
@@ -144,7 +155,7 @@ const Login = ( ): Node => {
|
||||
setPassword( text );
|
||||
}}
|
||||
value={password}
|
||||
secureTextEntry={true}
|
||||
secureTextEntry
|
||||
testID="Login.password"
|
||||
selectionColor={colors.black}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Button, Text, TextInput, View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Button, Text, TextInput, View
|
||||
} from "react-native";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/login/login";
|
||||
import { textStyles, viewStyles } from "../../styles/login/login";
|
||||
import { registerUser } from "./AuthenticationService";
|
||||
|
||||
const SignUp = (): Node => {
|
||||
@@ -47,7 +49,7 @@ const SignUp = (): Node => {
|
||||
style={viewStyles.input}
|
||||
onChangeText={setPassword}
|
||||
value={password}
|
||||
secureTextEntry={true}
|
||||
secureTextEntry
|
||||
/>
|
||||
<Button title="Register" onPress={register} />
|
||||
</View>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Image, Dimensions } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { Appbar, Button } from "react-native-paper";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dimensions, Image } from "react-native";
|
||||
import ImageZoom from "react-native-image-pan-zoom";
|
||||
import { Appbar, Button } from "react-native-paper";
|
||||
|
||||
import Photo from "../../models/Photo";
|
||||
import { imageStyles, viewStyles } from "../../styles/mediaViewer/mediaViewer";
|
||||
import DeletePhotoDialog from "../SharedComponents/DeletePhotoDialog";
|
||||
import PhotoCarousel from "../SharedComponents/PhotoCarousel";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import DeletePhotoDialog from "../SharedComponents/DeletePhotoDialog";
|
||||
import Photo from "../../models/Photo";
|
||||
|
||||
const { width, height } = Dimensions.get( "screen" );
|
||||
const selectedImageHeight = height - 350;
|
||||
@@ -24,14 +24,16 @@ type Props = {
|
||||
hideModal: Function
|
||||
}
|
||||
|
||||
const MediaViewer = ( { photoUris, setPhotoUris, initialPhotoSelected, hideModal }: Props ): Node => {
|
||||
const MediaViewer = ( {
|
||||
photoUris, setPhotoUris, initialPhotoSelected, hideModal
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState( initialPhotoSelected );
|
||||
const [deleteDialogVisible, setDeleteDialogVisible] = useState( false );
|
||||
|
||||
const numOfPhotos = photoUris.length;
|
||||
|
||||
const handlePhotoSelection = ( index ) => setSelectedPhotoIndex( index );
|
||||
const handlePhotoSelection = index => setSelectedPhotoIndex( index );
|
||||
|
||||
const showDialog = ( ) => setDeleteDialogVisible( true );
|
||||
const hideDialog = ( ) => setDeleteDialogVisible( false );
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { FlatList, Text, ActivityIndicator } from "react-native";
|
||||
import { ActivityIndicator, FlatList, Text } from "react-native";
|
||||
|
||||
import { textStyles } from "../../styles/messages/messages";
|
||||
|
||||
type Props = {
|
||||
@@ -15,20 +16,17 @@ const MessageList = ( {
|
||||
messageList,
|
||||
testID
|
||||
}: Props ): React.Node => {
|
||||
|
||||
if ( loading ) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
testID={"Messages.activityIndicator"}
|
||||
testID="Messages.activityIndicator"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderMessages = ( { item } ) => {
|
||||
return (
|
||||
<Text style={textStyles.projectName}>{item.subject}</Text>
|
||||
);
|
||||
};
|
||||
const renderMessages = ( { item } ) => (
|
||||
<Text style={textStyles.projectName}>{item.subject}</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
@@ -40,4 +38,3 @@ const MessageList = ( {
|
||||
};
|
||||
|
||||
export default MessageList;
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import MessageList from "./MessageList";
|
||||
import useMessages from "./hooks/useMessages";
|
||||
import MessageList from "./MessageList";
|
||||
|
||||
const Messages = ( ): Node => {
|
||||
|
||||
// TODO: Reload when accessing again
|
||||
const { messages, loading } = useMessages( );
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import MESSAGE_FIELDS from "../../../providers/fields";
|
||||
import { getJWTToken } from "../../LoginSignUp/AuthenticationService";
|
||||
import { MESSAGE_FIELDS } from "../../../providers/fields";
|
||||
|
||||
const useMessages = ( ): {
|
||||
messages: Array<Object>,
|
||||
@@ -37,7 +37,7 @@ const useMessages = ( ): {
|
||||
setLoading( false );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch messages:", e.message, );
|
||||
console.log( "Couldn't fetch messages:", e.message );
|
||||
setLoading( false );
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import NetworkLogger from "react-native-network-logger";
|
||||
import type { Node } from "react";
|
||||
|
||||
import ViewWithFooter from "./SharedComponents/ViewWithFooter";
|
||||
|
||||
@@ -14,4 +14,3 @@ const NetworkLogging = ( ): Node => (
|
||||
);
|
||||
|
||||
export default NetworkLogging;
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Text, View, Pressable } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
import SmallSquareImage from "./SmallSquareImage";
|
||||
import { textStyles, viewStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import Taxon from "../../models/Taxon";
|
||||
import User from "../../models/User";
|
||||
import KebabMenu from "./KebabMenu";
|
||||
import { timeAgo } from "../../sharedHelpers/dateAndTime";
|
||||
import { textStyles, viewStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import PlaceholderText from "../PlaceholderText";
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
import KebabMenu from "./KebabMenu";
|
||||
import SmallSquareImage from "./SmallSquareImage";
|
||||
|
||||
type Props = {
|
||||
item: Object,
|
||||
@@ -20,10 +20,12 @@ type Props = {
|
||||
toggleRefetch: Function
|
||||
}
|
||||
|
||||
const ActivityItem = ( { item, navToTaxonDetails, handlePress, toggleRefetch }: Props ): Node => {
|
||||
const ActivityItem = ( {
|
||||
item, navToTaxonDetails, handlePress, toggleRefetch
|
||||
}: Props ): Node => {
|
||||
const [currentUser, setCurrentUser] = useState( null );
|
||||
const taxon = item.taxon;
|
||||
const user = item.user;
|
||||
const { taxon } = item;
|
||||
const { user } = item;
|
||||
|
||||
useEffect( ( ) => {
|
||||
const isCurrentUser = async ( ) => {
|
||||
@@ -66,7 +68,11 @@ const ActivityItem = ( { item, navToTaxonDetails, handlePress, toggleRefetch }:
|
||||
<SmallSquareImage uri={Taxon.uri( taxon )} />
|
||||
<View>
|
||||
<Text style={textStyles.commonNameText}>{taxon.preferredCommonName}</Text>
|
||||
<Text style={textStyles.scientificNameText}>{taxon.rank} {taxon.name}</Text>
|
||||
<Text style={textStyles.scientificNameText}>
|
||||
{taxon.rank}
|
||||
{" "}
|
||||
{taxon.name}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { t } from "i18next";
|
||||
import * as React from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import ActivityItem from "./ActivityItem";
|
||||
|
||||
@@ -14,7 +14,9 @@ type Props = {
|
||||
toggleRefetch: Function
|
||||
}
|
||||
|
||||
const ActivityTab = ( { comments, ids, navToTaxonDetails, navToUserProfile, toggleRefetch }: Props ): React.Node => {
|
||||
const ActivityTab = ( {
|
||||
comments, ids, navToTaxonDetails, navToUserProfile, toggleRefetch
|
||||
}: Props ): React.Node => {
|
||||
if ( comments.length === 0 && ids.length === 0 ) {
|
||||
return <Text>{t( "No-comments-or-ids-to-display" )}</Text>;
|
||||
}
|
||||
@@ -28,7 +30,12 @@ const ActivityTab = ( { comments, ids, navToTaxonDetails, navToUserProfile, togg
|
||||
// https://github.com/inaturalist/inaturalist/blob/df6572008f60845b8ef5972a92a9afbde6f67829/app/webpack/observations/show/components/activity_item.jsx
|
||||
return (
|
||||
<View key={item.uuid}>
|
||||
<ActivityItem item={item} handlePress={handlePress} navToTaxonDetails={navToTaxonDetails} toggleRefetch={toggleRefetch} />
|
||||
<ActivityItem
|
||||
item={item}
|
||||
handlePress={handlePress}
|
||||
navToTaxonDetails={navToTaxonDetails}
|
||||
toggleRefetch={toggleRefetch}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} );
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { textStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import Map from "../SharedComponents/Map";
|
||||
import DropdownPicker from "../Explore/DropdownPicker";
|
||||
import Map from "../SharedComponents/Map";
|
||||
import addToProject from "./helpers/addToProject";
|
||||
import checkCamelAndSnakeCase from "./helpers/checkCamelAndSnakeCase";
|
||||
|
||||
@@ -23,7 +23,7 @@ const DataTab = ( { observation }: Props ): Node => {
|
||||
const attribution = observation.taxon && observation.taxon.default_photo
|
||||
&& observation.taxon.default_photo.attribution;
|
||||
|
||||
const selectProjectId = ( getValue ) => {
|
||||
const selectProjectId = getValue => {
|
||||
addToProject( getValue( ), observation.uuid );
|
||||
setProjectId( getValue( ) );
|
||||
};
|
||||
@@ -51,8 +51,12 @@ const DataTab = ( { observation }: Props ): Node => {
|
||||
{checkCamelAndSnakeCase( observation, "placeGuess" )}
|
||||
</Text>
|
||||
<Text style={textStyles.dataTabText}>{t( "Date" )}</Text>
|
||||
<Text style={textStyles.dataTabText}>{`${t( "Date-observed-colon" )} ${displayTimeObserved( )}`}</Text>
|
||||
<Text style={textStyles.dataTabText}>{`${t( "Date-uploaded-colon" )} ${observation._synced_at}`}</Text>
|
||||
<Text style={textStyles.dataTabText}>
|
||||
{`${t( "Date-observed-colon" )} ${displayTimeObserved( )}`}
|
||||
</Text>
|
||||
<Text style={textStyles.dataTabText}>
|
||||
{`${t( "Date-uploaded-colon" )} ${observation._synced_at}`}
|
||||
</Text>
|
||||
<Text style={textStyles.dataTabText}>{t( "Projects" )}</Text>
|
||||
{/* TODO: create a custom dropdown that doesn't use FlatList */}
|
||||
<DropdownPicker
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import type { Node } from "react";
|
||||
import { Button, Menu, Provider, DefaultTheme } from "react-native-paper";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Realm from "realm";
|
||||
import { View } from "react-native";
|
||||
import {
|
||||
Button, DefaultTheme, Menu, Provider
|
||||
} from "react-native-paper";
|
||||
import Realm from "realm";
|
||||
|
||||
import { viewStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import Comment from "../../models/Comment";
|
||||
import realmConfig from "../../models/index";
|
||||
import { colors } from "../../styles/global";
|
||||
import colors from "../../styles/colors";
|
||||
import { viewStyles } from "../../styles/obsDetails/obsDetails";
|
||||
|
||||
type Props = {
|
||||
uuid: string,
|
||||
@@ -35,7 +37,8 @@ const KebabMenu = ( { uuid, toggleRefetch }: Props ): Node => {
|
||||
contentStyle={viewStyles.textPadding}
|
||||
anchor={
|
||||
<Button onPress={openMenu} icon="dots-horizontal" textColor={colors.logInGray} />
|
||||
}>
|
||||
}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={async ( ) => {
|
||||
const realm = await Realm.open( realmConfig );
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
// @flow
|
||||
|
||||
import _ from "lodash";
|
||||
import React, {useState, useContext, useEffect} from "react";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { formatISO } from "date-fns";
|
||||
import { Text, View, Image, Pressable, ScrollView, LogBox, Alert } from "react-native";
|
||||
import _ from "lodash";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Alert, Image, LogBox, Pressable, ScrollView, Text, View
|
||||
} from "react-native";
|
||||
|
||||
import ActivityTab from "./ActivityTab";
|
||||
import checkCamelAndSnakeCase from "./helpers/checkCamelAndSnakeCase";
|
||||
import createComment from "./helpers/createComment";
|
||||
import createIdentification from "../Identify/helpers/createIdentification";
|
||||
import DataTab from "./DataTab";
|
||||
import faveObservation from "./helpers/faveObservation";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import ObsDetailsHeader from "./ObsDetailsHeader";
|
||||
import PhotoScroll from "../SharedComponents/PhotoScroll";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import Taxon from "../../models/Taxon";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import User from "../../models/User";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import { formatObsListTime } from "../../sharedHelpers/dateAndTime";
|
||||
import { textStyles, viewStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import createIdentification from "../Identify/helpers/createIdentification";
|
||||
import { getUser } from "../LoginSignUp/AuthenticationService";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import PhotoScroll from "../SharedComponents/PhotoScroll";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import { useRemoteObservation } from "./hooks/useRemoteObservation";
|
||||
import { viewStyles, textStyles } from "../../styles/obsDetails/obsDetails";
|
||||
import { formatObsListTime } from "../../sharedHelpers/dateAndTime";
|
||||
import { getUser } from "../LoginSignUp/AuthenticationService";
|
||||
import ActivityTab from "./ActivityTab";
|
||||
import DataTab from "./DataTab";
|
||||
import checkCamelAndSnakeCase from "./helpers/checkCamelAndSnakeCase";
|
||||
import createComment from "./helpers/createComment";
|
||||
import faveObservation from "./helpers/faveObservation";
|
||||
import useRemoteObservation from "./hooks/useRemoteObservation";
|
||||
import ObsDetailsHeader from "./ObsDetailsHeader";
|
||||
|
||||
// this is getting triggered by passing dates, like _created_at, through
|
||||
// react navigation via the observation object. it doesn't seem to
|
||||
@@ -43,7 +45,7 @@ const ObsDetails = ( ): Node => {
|
||||
const [comment, setComment] = useState( "" );
|
||||
const { addObservations } = useContext( ObsEditContext );
|
||||
const { params } = useRoute( );
|
||||
let observation = params.observation;
|
||||
let { observation } = params;
|
||||
const [tab, setTab] = useState( 0 );
|
||||
const navigation = useNavigation( );
|
||||
const [ids, setIds] = useState( [] );
|
||||
@@ -61,19 +63,16 @@ const ObsDetails = ( ): Node => {
|
||||
const toggleRefetch = ( ) => setRefetch( !refetch );
|
||||
|
||||
useEffect( () => {
|
||||
if ( observation ) {setIds( observation.identifications.map( i => i ) );}
|
||||
if ( observation ) { setIds( observation.identifications.map( i => i ) ); }
|
||||
}, [observation] );
|
||||
|
||||
if ( !observation ) { return null; }
|
||||
|
||||
|
||||
const comments = observation.comments.map( c => c );
|
||||
const photos = _.compact( observation.observationPhotos.map( op => op.photo ) );
|
||||
const user = observation.user;
|
||||
const taxon = observation.taxon;
|
||||
const uuid = observation.uuid;
|
||||
const { taxon, uuid, user } = observation;
|
||||
|
||||
const onIDAdded = async ( identification ) => {
|
||||
const onIDAdded = async identification => {
|
||||
console.log( "onIDAdded", identification );
|
||||
|
||||
// Add temporary ID to observation.identifications ("ghosted" ID, while we're trying to add it)
|
||||
@@ -89,20 +88,25 @@ const ObsDetails = ( ): Node => {
|
||||
created_at: formatISO( Date.now() ),
|
||||
uuid: identification.uuid,
|
||||
vision: false,
|
||||
// This tells us to render is ghosted (since it's temporarily visible until getting a response from the server)
|
||||
// This tells us to render is ghosted (since it's temporarily visible
|
||||
// until getting a response from the server)
|
||||
temporary: true
|
||||
};
|
||||
setIds( [ ...ids, newId ] );
|
||||
setIds( [...ids, newId] );
|
||||
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const results = await createIdentification( { observation_id: observation.uuid, taxon_id: newId.taxon.id, body: newId.body } );
|
||||
const results = await createIdentification( {
|
||||
observation_id: observation.uuid,
|
||||
taxon_id: newId.taxon.id,
|
||||
body: newId.body
|
||||
} );
|
||||
|
||||
if ( results === 1 ) {
|
||||
// Remove ghosted highlighting
|
||||
newId.temporary = false;
|
||||
setIds( [ ...ids, newId ] );
|
||||
setIds( [...ids, newId] );
|
||||
} else {
|
||||
// Couldn't create ID
|
||||
error = t( "Couldnt-create-identification", { error: t( "Unknown-error" ) } );
|
||||
@@ -130,7 +134,7 @@ const ObsDetails = ( ): Node => {
|
||||
const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", { id: taxon.id } );
|
||||
const navToAddID = ( ) => {
|
||||
addObservations( [observation] );
|
||||
navigation.push( "AddID", { onIDAdded: onIDAdded, goBackOnSave: true } );
|
||||
navigation.push( "AddID", { onIDAdded, goBackOnSave: true } );
|
||||
};
|
||||
const openCommentBox = ( ) => setShowCommentBox( true );
|
||||
const submitComment = async ( ) => {
|
||||
@@ -146,7 +150,7 @@ const ObsDetails = ( ): Node => {
|
||||
if ( !taxon ) { return <Text>{t( "Unknown-organism" )}</Text>; }
|
||||
return (
|
||||
<>
|
||||
<Image source={Taxon.uri( taxon )} style={viewStyles.imageBackground} />
|
||||
<Image source={Taxon.uri( taxon )} style={viewStyles.imageBackground} />
|
||||
<Pressable
|
||||
style={viewStyles.obsDetailsColumn}
|
||||
onPress={navToTaxonDetails}
|
||||
@@ -173,9 +177,9 @@ const ObsDetails = ( ): Node => {
|
||||
}
|
||||
};
|
||||
|
||||
const displayCreatedAt = ( ) => observation.createdAt
|
||||
const displayCreatedAt = ( ) => ( observation.createdAt
|
||||
? observation.createdAt
|
||||
: formatObsListTime( observation._created_at );
|
||||
: formatObsListTime( observation._created_at ) );
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
@@ -207,7 +211,9 @@ const ObsDetails = ( ): Node => {
|
||||
<View>
|
||||
<Text style={textStyles.text}>{observation.identifications.length}</Text>
|
||||
<Text style={textStyles.text}>{observation.comments.length}</Text>
|
||||
<Text style={textStyles.text}>{checkCamelAndSnakeCase( observation, "qualityGrade" )}</Text>
|
||||
<Text style={textStyles.text}>
|
||||
{checkCamelAndSnakeCase( observation, "qualityGrade" )}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={textStyles.locationText}>
|
||||
@@ -239,7 +245,7 @@ const ObsDetails = ( ): Node => {
|
||||
/>
|
||||
)
|
||||
: <DataTab observation={observation} />}
|
||||
<View style={viewStyles.row}>
|
||||
<View style={viewStyles.row}>
|
||||
<View style={viewStyles.button}>
|
||||
{/* TODO: get this button working. Not sure why createIdentification isn't working here
|
||||
but it doesn't appear to be working on staging either (Mar 11, 2022) */}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import type { Node } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Headline, Button } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { View } from "react-native";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Button, Headline } from "react-native-paper";
|
||||
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import colors from "../../styles/colors";
|
||||
import { viewStyles } from "../../styles/obsDetails/obsDetailsHeader";
|
||||
import { colors } from "../../styles/global";
|
||||
|
||||
type Props = {
|
||||
observationUUID: string
|
||||
|
||||
@@ -23,8 +23,9 @@ const addToProject = async ( projectId: number, obsId: string ): Promise<?number
|
||||
console.log( response, "response in project add" );
|
||||
return response.total_results;
|
||||
} catch ( e ) {
|
||||
console.log( "Couldn't add to project:", JSON.stringify( e.response ), );
|
||||
console.log( "Couldn't add to project:", JSON.stringify( e.response ) );
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default addToProject;
|
||||
|
||||
@@ -6,5 +6,4 @@ const checkCamelAndSnakeCase = ( object: Object, camelCaseKey: string ): ?string
|
||||
return object[camelCaseKey] || object[snakeCaseKey];
|
||||
};
|
||||
|
||||
|
||||
export default checkCamelAndSnakeCase;
|
||||
|
||||
@@ -22,8 +22,9 @@ const createComment = async ( body: string, uuid: string ): Promise<?number> =>
|
||||
const response = await inatjs.comments.create( apiParams, options );
|
||||
return response.total_results;
|
||||
} catch ( e ) {
|
||||
console.log( "Couldn't create comment:", JSON.stringify( e.response ), );
|
||||
console.log( "Couldn't create comment:", JSON.stringify( e.response ) );
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default createComment;
|
||||
|
||||
@@ -18,8 +18,9 @@ const faveObservation = async ( uuid: string, endpoint: string ): Promise<?numbe
|
||||
const response = await inatjs.observations[endpoint]( params, options );
|
||||
return response.total_results;
|
||||
} catch ( e ) {
|
||||
console.log( "Couldn't fave observation:", JSON.stringify( e.response ), );
|
||||
console.log( "Couldn't fave observation:", JSON.stringify( e.response ) );
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default faveObservation;
|
||||
|
||||
@@ -27,8 +27,9 @@ const flagObservation = async ( id: number ): Promise<?number> => {
|
||||
const response = await inatjs.flags.create( params, options );
|
||||
return response.total_results;
|
||||
} catch ( e ) {
|
||||
console.log( "Couldn't flag observation:", JSON.stringify( e.response ), );
|
||||
console.log( "Couldn't flag observation:", JSON.stringify( e.response ) );
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default flagObservation;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import Observation from "../../../models/Observation";
|
||||
import User from "../../../models/User";
|
||||
@@ -46,11 +46,11 @@ const useRemoteObservation = ( observation: Object, refetch: boolean ): Object =
|
||||
};
|
||||
|
||||
const response = await inatjs.observations.fetch( observation.uuid, params );
|
||||
const results = response.results;
|
||||
const { results } = response;
|
||||
const obs = Observation.mimicRealmMappedPropertiesSchema( results[0] );
|
||||
if ( !isCurrent ) { return; }
|
||||
if ( obs.faves ) {
|
||||
const userFavedObs = obs.faves.find( fave =>fave.user.login === currentUserLogin );
|
||||
const userFavedObs = obs.faves.find( fave => fave.user.login === currentUserLogin );
|
||||
if ( userFavedObs ) {
|
||||
setCurrentUserFaved( true );
|
||||
} else {
|
||||
@@ -60,7 +60,7 @@ const useRemoteObservation = ( observation: Object, refetch: boolean ): Object =
|
||||
setRemoteObservation( obs );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( `Couldn't fetch observation with uuid ${observation.uuid}: `, e.message, );
|
||||
console.log( `Couldn't fetch observation with uuid ${observation.uuid}: `, e.message );
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,6 +80,4 @@ const useRemoteObservation = ( observation: Object, refetch: boolean ): Object =
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
useRemoteObservation
|
||||
};
|
||||
export default useRemoteObservation;
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
View,
|
||||
Pressable,
|
||||
TouchableOpacity, FlatList, Image
|
||||
} from "react-native";
|
||||
import {useNavigation} from "@react-navigation/native";
|
||||
import { viewStyles, textStyles } from "../../styles/obsDetails/addID";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TextInput as NativeTextInput } from "react-native";
|
||||
import AddIDHeader from "./AddIDHeader";
|
||||
import {useRef, useState} from "react";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetModal,
|
||||
BottomSheetModalProvider
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import {Button, Headline, Text, TextInput} from "react-native-paper";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import {colors} from "../../styles/global";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as React from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable, TextInput as NativeTextInput, TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import {
|
||||
Button, Headline, Text, TextInput
|
||||
} from "react-native-paper";
|
||||
import uuid from "react-native-uuid";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import colors from "../../styles/colors";
|
||||
import { textStyles, viewStyles } from "../../styles/obsDetails/addID";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import AddIDHeader from "./AddIDHeader";
|
||||
|
||||
type Props = {
|
||||
route: {
|
||||
@@ -34,6 +37,11 @@ type Props = {
|
||||
}
|
||||
}
|
||||
|
||||
const SearchTaxonIcon = (
|
||||
<TextInput.Icon
|
||||
name={() => <Icon style={textStyles.taxonSearchIcon} name="magnify" size={25} />}
|
||||
/>
|
||||
);
|
||||
|
||||
const AddID = ( { route }: Props ): React.Node => {
|
||||
const { t } = useTranslation( );
|
||||
@@ -42,29 +50,34 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
const { onIDAdded, goBackOnSave, hideComment } = route.params;
|
||||
const bottomSheetModalRef = useRef( null );
|
||||
const [taxonSearch, setTaxonSearch] = useState( "" );
|
||||
const taxonList = useRemoteSearchResults( taxonSearch, "taxa", "taxon.name,taxon.preferred_common_name,taxon.default_photo.square_url,taxon.rank" );
|
||||
const taxonList = useRemoteSearchResults(
|
||||
taxonSearch,
|
||||
"taxa",
|
||||
"taxon.name,taxon.preferred_common_name,taxon.default_photo.square_url,taxon.rank"
|
||||
);
|
||||
const navigation = useNavigation( );
|
||||
|
||||
const renderBackdrop = ( props ) => (
|
||||
<BottomSheetBackdrop {...props} pressBehavior={"close"}
|
||||
appearsOnIndex={0}
|
||||
disappearsOnIndex={-1}
|
||||
const renderBackdrop = props => (
|
||||
<BottomSheetBackdrop
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
pressBehavior="close"
|
||||
appearsOnIndex={0}
|
||||
disappearsOnIndex={-1}
|
||||
/>
|
||||
);
|
||||
|
||||
const editComment = ( event ) => {
|
||||
const editComment = ( ) => {
|
||||
setCommentDraft( comment );
|
||||
bottomSheetModalRef.current?.present();
|
||||
};
|
||||
|
||||
const createPhoto = ( photo ) => {
|
||||
return {
|
||||
id: photo.id,
|
||||
url: photo.square_url
|
||||
};
|
||||
};
|
||||
const createPhoto = photo => ( {
|
||||
id: photo.id,
|
||||
url: photo.square_url
|
||||
} );
|
||||
|
||||
const createID = ( taxon ) => {
|
||||
const createID = taxon => {
|
||||
const newTaxon = {
|
||||
id: taxon.id,
|
||||
default_photo: taxon.default_photo ? createPhoto( taxon.default_photo ) : null,
|
||||
@@ -81,42 +94,72 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
return newID;
|
||||
};
|
||||
|
||||
const renderTaxonResult = ( {item} ) => {
|
||||
const taxonImage = item.default_photo ? { uri: item.default_photo.square_url } : Icon.getImageSourceSync( "leaf", 50, colors.inatGreen );
|
||||
const renderTaxonResult = ( { item } ) => {
|
||||
const taxonImage = item.default_photo
|
||||
? { uri: item.default_photo.square_url }
|
||||
: Icon.getImageSourceSync( "leaf", 50, colors.inatGreen );
|
||||
|
||||
return <View style={viewStyles.taxonResult} testID={`Search.taxa.${item.id}`}>
|
||||
<Image style={viewStyles.taxonResultIcon} source={taxonImage} testID={`Search.taxa.${item.id}.photo`}
|
||||
/>
|
||||
<View style={viewStyles.taxonResultNameContainer}>
|
||||
<Text style={textStyles.taxonResultName}>{item.name}</Text>
|
||||
<Text style={textStyles.taxonResultScientificName}>{item.preferred_common_name}</Text>
|
||||
return (
|
||||
<View style={viewStyles.taxonResult} testID={`Search.taxa.${item.id}`}>
|
||||
<Image
|
||||
style={viewStyles.taxonResultIcon}
|
||||
source={taxonImage}
|
||||
testID={`Search.taxa.${item.id}.photo`}
|
||||
/>
|
||||
<View style={viewStyles.taxonResultNameContainer}>
|
||||
<Text style={textStyles.taxonResultName}>{item.name}</Text>
|
||||
<Text style={textStyles.taxonResultScientificName}>{item.preferred_common_name}</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
style={viewStyles.taxonResultInfo}
|
||||
onPress={() => navigation.navigate( "TaxonDetails", { id: item.id } )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Icon style={textStyles.taxonResultInfoIcon} name="information-outline" size={25} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={viewStyles.taxonResultSelect}
|
||||
onPress={( ) => {
|
||||
onIDAdded( createID( item ) );
|
||||
if ( goBackOnSave ) { navigation.goBack(); }
|
||||
}}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Icon style={textStyles.taxonResultSelectIcon} name="check-bold" size={25} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable style={viewStyles.taxonResultInfo} onPress={() => navigation.navigate( "TaxonDetails", { id: item.id } )} accessibilityRole="link"><Icon style={textStyles.taxonResultInfoIcon} name="information-outline" size={25} /></Pressable>
|
||||
<Pressable style={viewStyles.taxonResultSelect} onPress={() => { onIDAdded( createID( item ) ); if ( goBackOnSave ) {navigation.goBack();} }} accessibilityRole="link"><Icon style={textStyles.taxonResultSelectIcon} name="check-bold" size={25} /></Pressable>
|
||||
</View>;
|
||||
);
|
||||
};
|
||||
|
||||
const taxonSearchIcon = ( ) => <Icon style={textStyles.taxonSearchIcon} name={"magnify"} size={25} />;
|
||||
|
||||
return (
|
||||
<BottomSheetModalProvider>
|
||||
<ViewNoFooter>
|
||||
<AddIDHeader showEditComment={!hideComment && comment.length === 0} onEditCommentPressed={editComment} />
|
||||
<AddIDHeader
|
||||
showEditComment={!hideComment && comment.length === 0}
|
||||
onEditCommentPressed={editComment}
|
||||
/>
|
||||
<View>
|
||||
<View style={viewStyles.scrollView}>
|
||||
{comment.length > 0 && <View>
|
||||
{comment.length > 0 && (
|
||||
<View>
|
||||
<Text>{t( "ID-Comment" )}</Text>
|
||||
<View style={viewStyles.commentContainer}>
|
||||
<Icon style={textStyles.commentLeftIcon} name="chat-processing-outline" size={25} />
|
||||
<Text style={textStyles.comment}>{comment}</Text>
|
||||
<Pressable style={viewStyles.commentRightIconContainer} onPress={editComment} accessibilityRole="link"><Icon style={textStyles.commentRightIcon} name="pencil" size={25} /></Pressable>
|
||||
<Pressable
|
||||
style={viewStyles.commentRightIconContainer}
|
||||
onPress={editComment}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Icon style={textStyles.commentRightIcon} name="pencil" size={25} />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
)}
|
||||
<Text>{t( "Search-Taxon-ID" )}</Text>
|
||||
<TextInput
|
||||
testID={"SearchTaxon"}
|
||||
left={<TextInput.Icon name={taxonSearchIcon} />}
|
||||
testID="SearchTaxon"
|
||||
left={SearchTaxonIcon}
|
||||
style={viewStyles.taxonSearch}
|
||||
value={taxonSearch}
|
||||
onChangeText={setTaxonSearch}
|
||||
@@ -140,7 +183,9 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
backdropComponent={renderBackdrop}
|
||||
style={viewStyles.bottomModal}
|
||||
>
|
||||
<Headline style={textStyles.commentHeader}>{comment.length > 0 ? t( "Edit-comment" ) : t( "Add-optional-comment" )}</Headline>
|
||||
<Headline style={textStyles.commentHeader}>
|
||||
{comment.length > 0 ? t( "Edit-comment" ) : t( "Add-optional-comment" )}
|
||||
</Headline>
|
||||
<View style={viewStyles.commentInputContainer}>
|
||||
<TextInput
|
||||
keyboardType="default"
|
||||
@@ -151,8 +196,9 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
autoFocus
|
||||
multiline
|
||||
onChangeText={setCommentDraft}
|
||||
render={( innerProps ) => (
|
||||
render={innerProps => (
|
||||
<NativeTextInput
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...innerProps}
|
||||
style={[
|
||||
innerProps.style,
|
||||
@@ -163,9 +209,16 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={viewStyles.commentClear}
|
||||
onPress={() => setCommentDraft( "" )}>
|
||||
onPress={() => setCommentDraft( "" )}
|
||||
>
|
||||
<Text
|
||||
style={[viewStyles.commentClearText, commentDraft.length === 0 ? textStyles.disabled : null]}>{t( "Clear" )}</Text>
|
||||
style={[
|
||||
viewStyles.commentClearText,
|
||||
commentDraft.length === 0 ? textStyles.disabled : null
|
||||
]}
|
||||
>
|
||||
{t( "Clear" )}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -176,7 +229,10 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
color={colors.midGray}
|
||||
onPress={() => {
|
||||
bottomSheetModalRef.current?.dismiss();
|
||||
}}>{t( "Cancel" )}</Button>
|
||||
}}
|
||||
>
|
||||
{t( "Cancel" )}
|
||||
</Button>
|
||||
<Button
|
||||
style={viewStyles.commentButton}
|
||||
uppercase={false}
|
||||
@@ -186,7 +242,10 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
onPress={() => {
|
||||
setComment( commentDraft );
|
||||
bottomSheetModalRef.current?.dismiss();
|
||||
}}>{comment.length > 0 ? t( "Edit-comment" ) : t( "Add-comment" )}</Button>
|
||||
}}
|
||||
>
|
||||
{comment.length > 0 ? t( "Edit-comment" ) : t( "Add-comment" )}
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheetModal>
|
||||
</ViewNoFooter>
|
||||
@@ -195,4 +254,3 @@ const AddID = ( { route }: Props ): React.Node => {
|
||||
};
|
||||
|
||||
export default AddID;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Node } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Headline } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import {Pressable, View} from "react-native";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Headline } from "react-native-paper";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
import { viewStyles } from "../../styles/obsDetails/addID";
|
||||
|
||||
type Props = {
|
||||
@@ -23,8 +24,16 @@ const AddIDHeader = ( { showEditComment, onEditCommentPressed }: Props ): Node =
|
||||
<View style={viewStyles.headerRow}>
|
||||
<HeaderBackButton onPress={( ) => navigation.goBack( )} />
|
||||
<Headline>{t( "Add-ID-Header" )}</Headline>
|
||||
{showEditComment ?
|
||||
<Pressable onPress={onEditCommentPressed} accessibilityRole="link"><Icon name="chat-processing-outline" size={25} /></Pressable> : <View />}
|
||||
{showEditComment
|
||||
? (
|
||||
<Pressable
|
||||
onPress={onEditCommentPressed}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Icon name="chat-processing-outline" size={25} />
|
||||
</Pressable>
|
||||
)
|
||||
: <View />}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { 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";
|
||||
import { viewStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import PlaceholderText from "../PlaceholderText";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
|
||||
const BottomModal = ( ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useState } from "react";
|
||||
import type { Node } from "react";
|
||||
import { View, Text, FlatList, ActivityIndicator, Pressable, Image } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { Searchbar } from "react-native-paper";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator, FlatList, Image, Pressable, Text, View
|
||||
} from "react-native";
|
||||
import { Searchbar } from "react-native-paper";
|
||||
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import useCVSuggestions from "./hooks/useCVSuggestions";
|
||||
import { viewStyles, textStyles } from "../../styles/obsEdit/cvSuggestions";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import useLoggedIn from "../../sharedHooks/useLoggedIn";
|
||||
import useRemoteObsEditSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { useLoggedIn } from "../../sharedHooks/useLoggedIn";
|
||||
import PhotoCarousel from "../SharedComponents/PhotoCarousel";
|
||||
import { textStyles, viewStyles } from "../../styles/obsEdit/cvSuggestions";
|
||||
import PlaceholderText from "../PlaceholderText";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import PhotoCarousel from "../SharedComponents/PhotoCarousel";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import useCVSuggestions from "./hooks/useCVSuggestions";
|
||||
|
||||
const CVSuggestions = ( ): Node => {
|
||||
const {
|
||||
@@ -32,7 +34,11 @@ const CVSuggestions = ( ): Node => {
|
||||
|
||||
const currentObs = observations[currentObsIndex];
|
||||
const hasPhotos = currentObs.observationPhotos;
|
||||
const { suggestions, status } = useCVSuggestions( currentObs, showSeenNearby, selectedPhotoIndex );
|
||||
const { suggestions, status } = useCVSuggestions(
|
||||
currentObs,
|
||||
showSeenNearby,
|
||||
selectedPhotoIndex
|
||||
);
|
||||
|
||||
const renderNavButtons = ( updateIdentification, id ) => {
|
||||
const navToTaxonDetails = ( ) => navigation.navigate( "TaxonDetails", { id } );
|
||||
@@ -52,7 +58,9 @@ const CVSuggestions = ( ): Node => {
|
||||
const renderSuggestions = ( { item } ) => {
|
||||
const taxon = item && item.taxon;
|
||||
// destructuring so this doesn't cause a crash
|
||||
const mediumUrl = ( taxon && taxon.taxon_photos && taxon.taxon_photos[0].photo ) ? taxon.taxon_photos[0].photo.medium_url : null;
|
||||
const mediumUrl = ( taxon && taxon.taxon_photos && taxon.taxon_photos[0].photo )
|
||||
? taxon.taxon_photos[0].photo.medium_url
|
||||
: null;
|
||||
const uri = { uri: mediumUrl };
|
||||
|
||||
const updateIdentification = ( ) => updateTaxon( taxon );
|
||||
@@ -104,12 +112,23 @@ const CVSuggestions = ( ): Node => {
|
||||
|
||||
const emptySuggestionsList = ( ) => {
|
||||
if ( !isLoggedIn ) {
|
||||
return <PlaceholderText style={[textStyles.explainerText]} text="you must be logged in to see computer vision suggestions" />;
|
||||
} else if ( status === "no_results" ) {
|
||||
return <PlaceholderText style={[textStyles.explainerText]} text="no computervision suggestions found" />;
|
||||
} else {
|
||||
return <ActivityIndicator />;
|
||||
return (
|
||||
<PlaceholderText
|
||||
style={[textStyles.explainerText]}
|
||||
text="you must be logged in to see computer vision suggestions"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if ( status === "no_results" ) {
|
||||
return (
|
||||
<PlaceholderText
|
||||
style={[textStyles.explainerText]}
|
||||
text="no computervision suggestions found"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ActivityIndicator />;
|
||||
};
|
||||
|
||||
const displaySuggestions = ( ) => (
|
||||
@@ -127,13 +146,9 @@ const CVSuggestions = ( ): Node => {
|
||||
/>
|
||||
);
|
||||
|
||||
const displayPhotos = ( ) => {
|
||||
return currentObs.observationPhotos.map( p => {
|
||||
return {
|
||||
uri: p.photo?.url || p?.photo?.localFilePath
|
||||
};
|
||||
} );
|
||||
};
|
||||
const displayPhotos = ( ) => currentObs.observationPhotos.map( p => ( {
|
||||
uri: p.photo?.url || p?.photo?.localFilePath
|
||||
} ) );
|
||||
|
||||
return (
|
||||
<ViewNoFooter>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Text, Pressable } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pressable, Text } from "react-native";
|
||||
|
||||
import { textStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import { displayDateTimeObsEdit } from "../../sharedHelpers/dateAndTime";
|
||||
import { textStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import DateTimePicker from "../SharedComponents/DateTimePicker";
|
||||
|
||||
type Props = {
|
||||
@@ -21,7 +21,7 @@ const DatePicker = ( { handleDatePicked, currentObs }: Props ): Node => {
|
||||
const openModal = () => setShowModal( true );
|
||||
const closeModal = () => setShowModal( false );
|
||||
|
||||
const handlePicked = ( value ) => {
|
||||
const handlePicked = value => {
|
||||
handleDatePicked( value );
|
||||
closeModal();
|
||||
};
|
||||
@@ -29,7 +29,7 @@ const DatePicker = ( { handleDatePicked, currentObs }: Props ): Node => {
|
||||
const displayDate = ( ) => {
|
||||
if ( currentObs.observed_on_string ) {
|
||||
return displayDateTimeObsEdit( currentObs.observed_on_string );
|
||||
} else if ( currentObs.time_observed_at ) {
|
||||
} if ( currentObs.time_observed_at ) {
|
||||
// this is for observations already uploaded to iNat
|
||||
return displayDateTimeObsEdit( currentObs.time_observed_at );
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import { Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text } from "react-native";
|
||||
|
||||
import { textStyles } from "../../styles/obsEdit/obsEdit";
|
||||
// import LocationPicker from "./LocationPicker";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import DatePicker from "./DatePicker";
|
||||
import { createObservedOnStringForUpload } from "../../sharedHelpers/dateAndTime";
|
||||
import { textStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import PhotoCarousel from "../SharedComponents/PhotoCarousel";
|
||||
import DatePicker from "./DatePicker";
|
||||
|
||||
type Props = {
|
||||
handleSelection: Function,
|
||||
@@ -58,7 +58,7 @@ const EvidenceSection = ( {
|
||||
// setObservations( updatedObs );
|
||||
// };
|
||||
|
||||
const handleDatePicked = ( selectedDate ) => {
|
||||
const handleDatePicked = selectedDate => {
|
||||
if ( selectedDate ) {
|
||||
const dateString = createObservedOnStringForUpload( selectedDate );
|
||||
updateObservedOn( dateString );
|
||||
@@ -95,7 +95,10 @@ const EvidenceSection = ( {
|
||||
photoUris={photoUris}
|
||||
setSelectedPhotoIndex={handleSelection}
|
||||
/>
|
||||
{/* TODO: bring back the location picker when it works on Android and allows navigation back */}
|
||||
{/*
|
||||
TODO: bring back the location picker when it works on Android and
|
||||
allows navigation back
|
||||
*/}
|
||||
{/* renderLocationPickerModal( ) */}
|
||||
{/*
|
||||
<Pressable
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import {Text, Pressable, FlatList, View} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
FlatList, Pressable, Text, View
|
||||
} from "react-native";
|
||||
import { Avatar, useTheme } from "react-native-paper";
|
||||
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import { textStyles, viewStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import { iconicTaxaIds, iconicTaxaNames } from "../../dictionaries/iconicTaxaIds";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import { textStyles, viewStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import PlaceholderText from "../PlaceholderText";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
|
||||
const IdentificationSection = ( ): Node => {
|
||||
const {
|
||||
@@ -26,14 +28,14 @@ const IdentificationSection = ( ): Node => {
|
||||
const currentObs = observations[currentObsIndex];
|
||||
const identification = currentObs.taxon;
|
||||
|
||||
const onIDAdded = async ( id ) => {
|
||||
const updateIdentification = taxon => updateTaxon( taxon );
|
||||
|
||||
const onIDAdded = async id => {
|
||||
console.log( "onIDAdded", id );
|
||||
updateIdentification( id.taxon );
|
||||
};
|
||||
|
||||
const updateIdentification = ( taxon ) => updateTaxon( taxon );
|
||||
|
||||
const navToAddID = ( ) => navigation.push( "AddID", { onIDAdded: onIDAdded, hideComment: true } );
|
||||
const navToAddID = ( ) => navigation.push( "AddID", { onIDAdded, hideComment: true } );
|
||||
|
||||
const renderIconicTaxaButton = ( { item } ) => {
|
||||
const id = iconicTaxaIds[item];
|
||||
@@ -76,30 +78,29 @@ const IdentificationSection = ( ): Node => {
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<RoundGreenButton
|
||||
handlePress={navToAddID}
|
||||
buttonText="View Identification Suggestions"
|
||||
testID="ObsEdit.Suggestions"
|
||||
/>
|
||||
<Text style={textStyles.text}>
|
||||
{identification && identification.id && t( iconicTaxaNames[identification.id] )}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<RoundGreenButton
|
||||
handlePress={navToAddID}
|
||||
buttonText="View Identification Suggestions"
|
||||
testID="ObsEdit.Suggestions"
|
||||
/>
|
||||
<Text style={textStyles.text}>
|
||||
{identification && identification.id && t( iconicTaxaNames[identification.id] )}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{displayIdentification( )}
|
||||
<FlatList
|
||||
data={Object.keys( iconicTaxaIds )}
|
||||
horizontal
|
||||
renderItem={renderIconicTaxaButton}
|
||||
/>
|
||||
<FlatList
|
||||
data={Object.keys( iconicTaxaIds )}
|
||||
horizontal
|
||||
renderItem={renderIconicTaxaButton}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import Map from "../SharedComponents/Map";
|
||||
import useLocationName from "../../sharedHooks/useLocationName";
|
||||
import { viewStyles } from "../../styles/obsEdit/locationPicker";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import useLocationName from "../../sharedHooks/useLocationName";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import Map from "../SharedComponents/Map";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import useCoords from "./hooks/useCoords";
|
||||
|
||||
type Props = {
|
||||
@@ -44,7 +44,7 @@ const LocationPicker = ( { closeLocationPicker, updateLocation }: Props ): Node
|
||||
}
|
||||
}, [newCoords, region, searchQuery] );
|
||||
|
||||
const updateCoords = ( newMapRegion ) => {
|
||||
const updateCoords = newMapRegion => {
|
||||
setSearchQuery( "" );
|
||||
setRegion( newMapRegion );
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Keyboard } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Keyboard } from "react-native";
|
||||
import { TextInput } from "react-native-paper";
|
||||
|
||||
import colors from "../../styles/colors";
|
||||
import { textStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import { colors } from "../../styles/global";
|
||||
|
||||
type Props = {
|
||||
addNotes: Function,
|
||||
@@ -18,10 +18,10 @@ const Notes = ( { addNotes, description }: Props ): Node => {
|
||||
const [keyboardOffset, setKeyboardOffset] = useState( 0 );
|
||||
|
||||
useEffect( ( ) => {
|
||||
const showSubscription = Keyboard.addListener( "keyboardDidShow", ( e ) => {
|
||||
const showSubscription = Keyboard.addListener( "keyboardDidShow", e => {
|
||||
setKeyboardOffset( e.endCoordinates.height );
|
||||
} );
|
||||
const hideSubscription = Keyboard.addListener( "keyboardDidHide", ( e ) => {
|
||||
const hideSubscription = Keyboard.addListener( "keyboardDidHide", ( ) => {
|
||||
setKeyboardOffset( 0 );
|
||||
} );
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Text, Pressable, View } from "react-native";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { Headline, Portal, Modal } from "react-native-paper";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { Headline, Modal, Portal } from "react-native-paper";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
import ScrollNoFooter from "../SharedComponents/ScrollNoFooter";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import { textStyles, viewStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import Photo from "../../models/Photo";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import { useLoggedIn } from "../../sharedHooks/useLoggedIn";
|
||||
import useLoggedIn from "../../sharedHooks/useLoggedIn";
|
||||
import { textStyles, viewStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import MediaViewer from "../MediaViewer/MediaViewer";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import ScrollNoFooter from "../SharedComponents/ScrollNoFooter";
|
||||
import EvidenceSection from "./EvidenceSection";
|
||||
import IdentificationSection from "./IdentificationSection";
|
||||
import OtherDataSection from "./OtherDataSection";
|
||||
import EvidenceSection from "./EvidenceSection";
|
||||
import MediaViewer from "../MediaViewer/MediaViewer";
|
||||
import Photo from "../../models/Photo";
|
||||
|
||||
const ObsEdit = ( ): Node => {
|
||||
const {
|
||||
@@ -46,8 +46,8 @@ const ObsEdit = ( ): Node => {
|
||||
const showNextObservation = ( ) => setCurrentObsIndex( currentObsIndex + 1 );
|
||||
const showPrevObservation = ( ) => setCurrentObsIndex( currentObsIndex - 1 );
|
||||
|
||||
const renderArrowNavigation = ( ) => {
|
||||
if ( observations.length === 0 ) { return; }
|
||||
const renderArrowNavigation = ( ): Node | null => {
|
||||
if ( observations.length === 0 ) { return null; }
|
||||
|
||||
const handleBackButtonPress = ( ) => {
|
||||
if ( lastScreen === "StandardCamera" ) {
|
||||
@@ -89,32 +89,33 @@ const ObsEdit = ( ): Node => {
|
||||
);
|
||||
};
|
||||
|
||||
const setPhotos = ( uris ) => {
|
||||
const currentObs = observations[currentObsIndex];
|
||||
|
||||
const setPhotos = uris => {
|
||||
const updatedObservations = observations;
|
||||
const updatedObsPhotos = currentObs.observationPhotos.filter( obsPhoto => {
|
||||
const { photo } = obsPhoto;
|
||||
if ( uris.includes( photo.url || photo.localFilePath ) ) {
|
||||
return obsPhoto;
|
||||
}
|
||||
return false;
|
||||
} );
|
||||
currentObs.observationPhotos = updatedObsPhotos;
|
||||
setObservations( [...updatedObservations] );
|
||||
};
|
||||
|
||||
const handleSelection = ( photo ) => {
|
||||
const handleSelection = photo => {
|
||||
setInitialPhotoSelected( photo );
|
||||
showModal( );
|
||||
};
|
||||
|
||||
const currentObs = observations[currentObsIndex];
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( !currentObs || !currentObs.observationPhotos ) { return; }
|
||||
const uris = currentObs.observationPhotos.map( ( obsPhoto => {
|
||||
return Photo.displayLocalOrRemoteSquarePhoto( obsPhoto.photo );
|
||||
} ) );
|
||||
const uris = currentObs.observationPhotos.map(
|
||||
obsPhoto => Photo.displayLocalOrRemoteSquarePhoto( obsPhoto.photo )
|
||||
);
|
||||
setPhotoUris( uris );
|
||||
}, [currentObs ] );
|
||||
}, [currentObs] );
|
||||
|
||||
if ( !currentObs ) { return null; }
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { FlatList, Pressable, Text, Image } from "react-native";
|
||||
import {
|
||||
FlatList, Image, Pressable, Text
|
||||
} from "react-native";
|
||||
|
||||
import useRemoteObsEditSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { imageStyles, viewStyles } from "../../styles/search/search";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import { viewStyles, imageStyles } from "../../styles/search/search";
|
||||
|
||||
type Props = {
|
||||
source: string,
|
||||
@@ -31,26 +33,29 @@ const ObsEditSearch = ( {
|
||||
style={viewStyles.row}
|
||||
testID={`ObsEditSearch.taxa.${item.id}`}
|
||||
>
|
||||
<Image source={imageUrl} style={imageStyles.squareImage} testID={`ObsEditSearch.taxa.${item.id}.photo`} />
|
||||
<Image
|
||||
source={imageUrl}
|
||||
style={imageStyles.squareImage}
|
||||
testID={`ObsEditSearch.taxa.${item.id}.photo`}
|
||||
/>
|
||||
<Text>{`${item.preferred_common_name} (${item.rank} ${item.name})`}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={( ) => handlePress( item.id )}
|
||||
style={viewStyles.row}
|
||||
testID={`ObsEditSearch.project.${item.id}`}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item.icon }}
|
||||
style={imageStyles.squareImage}
|
||||
testID={`ObsEditSearch.project.${item.id}.photo`}
|
||||
/>
|
||||
<Text>{item.title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Pressable
|
||||
onPress={( ) => handlePress( item.id )}
|
||||
style={viewStyles.row}
|
||||
testID={`ObsEditSearch.project.${item.id}`}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item.icon }}
|
||||
style={imageStyles.squareImage}
|
||||
testID={`ObsEditSearch.project.${item.id}.photo`}
|
||||
/>
|
||||
<Text>{item.title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import type { Node } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text, View } from "react-native";
|
||||
import RNPickerSelect from "react-native-picker-select";
|
||||
import type { Node } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { pickerSelectStyles, textStyles, viewStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import { ObsEditContext } from "../../providers/contexts";
|
||||
import { pickerSelectStyles, textStyles, viewStyles } from "../../styles/obsEdit/obsEdit";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import Notes from "./Notes";
|
||||
|
||||
|
||||
const OtherDataSection = ( ): Node => {
|
||||
const {
|
||||
currentObsIndex,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs, { FileUpload } from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getJWTToken } from "../../LoginSignUp/AuthenticationService";
|
||||
import Photo from "../../../models/Photo";
|
||||
import { getJWTToken } from "../../LoginSignUp/AuthenticationService";
|
||||
|
||||
const TAXON_FIELDS = {
|
||||
name: true,
|
||||
@@ -16,22 +16,27 @@ const PHOTO_FIELDS = {
|
||||
};
|
||||
|
||||
const FIELDS = {
|
||||
taxon: Object.assign( {}, TAXON_FIELDS, {
|
||||
taxon: {
|
||||
...TAXON_FIELDS,
|
||||
taxon_photos: {
|
||||
photo: PHOTO_FIELDS
|
||||
}
|
||||
} )
|
||||
}
|
||||
};
|
||||
|
||||
const useCVSuggestions = ( currentObs: Object, showSeenNearby: boolean, selectedPhoto: number ): Object => {
|
||||
const useCVSuggestions = (
|
||||
currentObs: Object,
|
||||
showSeenNearby: boolean,
|
||||
selectedPhoto: number
|
||||
): Object => {
|
||||
const [suggestions, setSuggestions] = useState( [] );
|
||||
const [status, setStatus] = useState( null );
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( !currentObs || !currentObs.observationPhotos ) { return; }
|
||||
if ( !currentObs || !currentObs.observationPhotos ) { return ( ) => { }; }
|
||||
const uri = currentObs.observationPhotos && currentObs.observationPhotos[selectedPhoto].uri;
|
||||
const latitude = currentObs.latitude;
|
||||
const longitude = currentObs.longitude;
|
||||
const { latitude } = currentObs;
|
||||
const { longitude } = currentObs;
|
||||
|
||||
let isCurrent = true;
|
||||
const fetchCVSuggestions = async ( ): Promise<Object> => {
|
||||
@@ -70,7 +75,6 @@ const useCVSuggestions = ( currentObs: Object, showSeenNearby: boolean, selected
|
||||
if ( !isCurrent ) { return; }
|
||||
} catch ( e ) {
|
||||
console.log( JSON.stringify( e.response ), "couldn't fetch CV suggestions" );
|
||||
if ( !isCurrent ) { return; }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,5 +91,3 @@ const useCVSuggestions = ( currentObs: Object, showSeenNearby: boolean, selected
|
||||
};
|
||||
|
||||
export default useCVSuggestions;
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ const useCoords = ( location: string ): Object => {
|
||||
} );
|
||||
} catch ( e ) {
|
||||
console.log( e, "couldn't fetch coords by location name" );
|
||||
if ( !isCurrent ) { return; }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,5 +42,3 @@ const useCoords = ( location: string ): Object => {
|
||||
};
|
||||
|
||||
export default useCoords;
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Pressable } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pressable } from "react-native";
|
||||
import { Text } from "react-native-paper";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/observations/loggedOutCard";
|
||||
import { textStyles, viewStyles } from "../../styles/observations/loggedOutCard";
|
||||
|
||||
type Props = {
|
||||
numObsToUpload: number
|
||||
@@ -20,7 +20,9 @@ const LoggedOutCard = ( { numObsToUpload }: Props ): Node => {
|
||||
return (
|
||||
<Pressable style={viewStyles.loggedOutCard} onPress={( ) => navigation.navigate( "login" )}>
|
||||
<Text variant="titleLarge" style={textStyles.centerText}>{t( "Log-in-to-iNaturalist" )}</Text>
|
||||
<Text variant="bodyLarge" style={textStyles.centerText}>{t( "X-unuploaded-observations", { observationCount: numObsToUpload } )}</Text>
|
||||
<Text variant="bodyLarge" style={textStyles.centerText}>
|
||||
{t( "X-unuploaded-observations", { observationCount: numObsToUpload } )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { Button } from "react-native-paper";
|
||||
import { t } from "i18next";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/observations/obsList";
|
||||
import { colors } from "../../styles/global";
|
||||
import colors from "../../styles/colors";
|
||||
import { textStyles, viewStyles } from "../../styles/observations/obsList";
|
||||
|
||||
const LoginPrompt = ( ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
// @flow
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import type { Node } from "react";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import ObservationViews from "../SharedComponents/ObservationViews/ObservationViews";
|
||||
import UserCard from "./UserCard";
|
||||
import { useCurrentUser } from "./hooks/useCurrentUser";
|
||||
import BottomSheet from "../SharedComponents/BottomSheet";
|
||||
import ObservationViews from "../SharedComponents/ObservationViews/ObservationViews";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import useUser from "../UserProfile/hooks/useUser";
|
||||
import useCurrentUser from "./hooks/useCurrentUser";
|
||||
import useObservations from "./hooks/useObservations";
|
||||
import LoggedOutCard from "./LoggedOutCard";
|
||||
import { useUser } from "../UserProfile/hooks/useUser";
|
||||
import LoginPrompt from "./LoginPrompt";
|
||||
import UploadPrompt from "./UploadPrompt";
|
||||
import UserCard from "./UserCard";
|
||||
|
||||
const ObsList = ( ): Node => {
|
||||
const { params } = useRoute( );
|
||||
const { observationList, loading, syncObservations, fetchNextObservations, obsToUpload } = useObservations( );
|
||||
const {
|
||||
observationList, loading, syncObservations, fetchNextObservations, obsToUpload
|
||||
} = useObservations( );
|
||||
|
||||
const id = params && params.userId;
|
||||
const userId = useCurrentUser( ) || id;
|
||||
@@ -36,7 +38,11 @@ const ObsList = ( ): Node => {
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
{user ? <UserCard userId={userId} user={user} /> : <LoggedOutCard numObsToUpload={numObsToUpload} />}
|
||||
{
|
||||
user
|
||||
? <UserCard userId={userId} user={user} />
|
||||
: <LoggedOutCard numObsToUpload={numObsToUpload} />
|
||||
}
|
||||
<ObservationViews
|
||||
loading={loading}
|
||||
observationList={observationList}
|
||||
@@ -49,8 +55,7 @@ const ObsList = ( ): Node => {
|
||||
<BottomSheet>
|
||||
{!userId
|
||||
? <LoginPrompt />
|
||||
: <UploadPrompt obsToUpload={obsToUpload} />
|
||||
}
|
||||
: <UploadPrompt obsToUpload={obsToUpload} />}
|
||||
</BottomSheet>
|
||||
)}
|
||||
</ViewWithFooter>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { Button } from "react-native-paper";
|
||||
import { t } from "i18next";
|
||||
import uploadObservation from "../../providers/uploadHelpers/uploadObservation";
|
||||
import Observation from "../../models/Observation";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/observations/obsList";
|
||||
import { colors } from "../../styles/global";
|
||||
import Observation from "../../models/Observation";
|
||||
import uploadObservation from "../../providers/uploadHelpers/uploadObservation";
|
||||
import colors from "../../styles/colors";
|
||||
import { textStyles, viewStyles } from "../../styles/observations/obsList";
|
||||
|
||||
type Props = {
|
||||
obsToUpload: Array<Object>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Text, View, Pressable } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
import User from "../../models/User";
|
||||
import { viewStyles } from "../../styles/observations/userCard";
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
|
||||
type Props = {
|
||||
userId: number,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getJWTToken } from "../../../components/LoginSignUp/AuthenticationService";
|
||||
import { getJWTToken } from "../../LoginSignUp/AuthenticationService";
|
||||
|
||||
const useCurrentUser = ( ): Object => {
|
||||
const [currentUser, setCurrentUser] = useState( null );
|
||||
@@ -21,7 +21,7 @@ const useCurrentUser = ( ): Object => {
|
||||
api_token: apiToken
|
||||
};
|
||||
const response = await inatjs.users.me( options );
|
||||
const results = response.results;
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
setCurrentUser( results[0].id );
|
||||
} catch ( e ) {
|
||||
@@ -39,6 +39,4 @@ const useCurrentUser = ( ): Object => {
|
||||
return currentUser;
|
||||
};
|
||||
|
||||
export {
|
||||
useCurrentUser
|
||||
};
|
||||
export default useCurrentUser;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useCallback, useRef, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import {
|
||||
useCallback, useEffect, useRef, useState
|
||||
} from "react";
|
||||
import Realm from "realm";
|
||||
|
||||
import realmConfig from "../../../models/index";
|
||||
import Observation from "../../../models/Observation";
|
||||
import { getUsername, getUserId } from "../../../components/LoginSignUp/AuthenticationService";
|
||||
import { getUserId, getUsername } from "../../LoginSignUp/AuthenticationService";
|
||||
|
||||
const perPage = 6;
|
||||
|
||||
const useObservations = ( ): Object => {
|
||||
const [loading, setLoading] = useState( true );
|
||||
const [observationList, setObservationList] = useState( [] );
|
||||
const nextPageToFetch = observationList.length > 0 ? Math.ceil( observationList.length / perPage ) : 1;
|
||||
const nextPageToFetch = observationList.length > 0
|
||||
? Math.ceil( observationList.length / perPage )
|
||||
: 1;
|
||||
const [page, setPage] = useState( nextPageToFetch );
|
||||
const [userLogin, setUserLogin] = useState( null );
|
||||
const [obsToUpload, setObsToUpload] = useState( [] );
|
||||
@@ -39,12 +43,14 @@ const useObservations = ( ): Object => {
|
||||
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.
|
||||
// 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 obs = realm.objects( "Observation" );
|
||||
const localObservations = obs.sorted( "_created_at", true );
|
||||
|
||||
// includes obs which have never been synced or which have been updated locally since the last sync
|
||||
// includes obs which have never been synced or which have been updated
|
||||
// locally since the last sync
|
||||
const notUploadedObs = obs.filtered( "_synced_at == null || _synced_at <= _updated_at" );
|
||||
|
||||
if ( localObservations?.length ) {
|
||||
@@ -90,7 +96,7 @@ const useObservations = ( ): Object => {
|
||||
return closeRealm;
|
||||
}, [openRealm, closeRealm] );
|
||||
|
||||
const writeToDatabase = useCallback( ( results ) => {
|
||||
const writeToDatabase = useCallback( results => {
|
||||
if ( results.length === 0 ) { return; }
|
||||
const realm = realmRef.current;
|
||||
results.forEach( obs => {
|
||||
@@ -128,13 +134,13 @@ const useObservations = ( ): Object => {
|
||||
fields: Observation.FIELDS
|
||||
};
|
||||
const response = await inatjs.observations.search( params );
|
||||
const results = response.results;
|
||||
const { results } = response;
|
||||
if ( !isCurrent ) { return; }
|
||||
writeToDatabase( results );
|
||||
} catch ( e ) {
|
||||
setLoading( false );
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch observations:", e.message, );
|
||||
console.log( "Couldn't fetch observations:", e.message );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Pressable, Image, FlatList, ActivityIndicator, Text, View } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator, FlatList, Image, Pressable, Text, View
|
||||
} from "react-native";
|
||||
import Realm from "realm";
|
||||
|
||||
import { imageStyles, viewStyles, textStyles } from "../../styles/photoLibrary/photoGallery";
|
||||
import GroupPhotosHeader from "./GroupPhotosHeader";
|
||||
import realmConfig from "../../models/index";
|
||||
import Observation from "../../models/Observation";
|
||||
import { ObsEditContext, PhotoGalleryContext } from "../../providers/contexts";
|
||||
import { imageStyles, textStyles, viewStyles } from "../../styles/photoLibrary/photoGallery";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import GroupPhotosFooter from "./GroupPhotosFooter";
|
||||
import Observation from "../../models/Observation";
|
||||
import { orderByTimestamp, flattenAndOrderSelectedPhotos } from "./helpers/groupPhotoHelpers";
|
||||
import GroupPhotosHeader from "./GroupPhotosHeader";
|
||||
import { flattenAndOrderSelectedPhotos, orderByTimestamp } from "./helpers/groupPhotoHelpers";
|
||||
|
||||
const GroupPhotos = ( ): Node => {
|
||||
const { addObservations } = useContext( ObsEditContext );
|
||||
@@ -24,13 +28,13 @@ const GroupPhotos = ( ): Node => {
|
||||
const [obsToEdit, setObsToEdit] = useState( { observations } );
|
||||
const [selectedObservations, setSelectedObservations] = useState( [] );
|
||||
|
||||
const updateFlatList = ( rerenderFlatList ) => {
|
||||
const updateFlatList = rerenderFlatList => {
|
||||
setObsToEdit( {
|
||||
...obsToEdit,
|
||||
// there might be a better way to do this, but adding this key forces the FlatList
|
||||
// to rerender anytime an observation is unselected
|
||||
rerenderFlatList
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
const selectObservationPhotos = ( isSelected, observation ) => {
|
||||
@@ -128,7 +132,7 @@ const GroupPhotos = ( ): Node => {
|
||||
// make sure at least one set of combined photos is selected
|
||||
if ( maxCombinedPhotos < 2 ) { return; }
|
||||
|
||||
let separatedPhotos = [];
|
||||
const separatedPhotos = [];
|
||||
const orderedPhotos = flattenAndOrderSelectedPhotos( selectedObservations );
|
||||
|
||||
// create a list of grouped photos, with selected photos split into individual observations
|
||||
@@ -148,15 +152,17 @@ const GroupPhotos = ( ): Node => {
|
||||
};
|
||||
|
||||
const removePhotos = ( ) => {
|
||||
let removedPhotos = {};
|
||||
let removedFromGroup = [];
|
||||
const removedPhotos = {};
|
||||
const removedFromGroup = [];
|
||||
|
||||
const orderedPhotos = flattenAndOrderSelectedPhotos( );
|
||||
|
||||
// create a list of selected photos in each album, with selected photos removed
|
||||
albums.forEach( album => {
|
||||
const currentAlbum = selectedPhotos[album];
|
||||
const filteredAlbum = currentAlbum && currentAlbum.filter( item => !orderedPhotos.includes( item ) );
|
||||
const filteredAlbum = currentAlbum && currentAlbum.filter(
|
||||
item => !orderedPhotos.includes( item )
|
||||
);
|
||||
removedPhotos.album = filteredAlbum;
|
||||
} );
|
||||
|
||||
@@ -177,7 +183,9 @@ const GroupPhotos = ( ): Node => {
|
||||
|
||||
const navToObsEdit = async ( ) => {
|
||||
const obs = obsToEdit.observations;
|
||||
const obsPhotos = await Observation.createMutipleObsFromGalleryPhotos( obs );
|
||||
const realm = await Realm.open( realmConfig );
|
||||
const obsPhotos = await Observation.createMutipleObsFromGalleryPhotos( obs, realm );
|
||||
realm.close( );
|
||||
addObservations( obsPhotos );
|
||||
navigation.navigate( "ObsEdit" );
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { View, Pressable } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/photoLibrary/photoGalleryHeader";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
import Modal from "../SharedComponents/Modal";
|
||||
import { textStyles, viewStyles } from "../../styles/photoLibrary/photoGalleryHeader";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import Modal from "../SharedComponents/Modal";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
|
||||
type Props = {
|
||||
combinePhotos: Function,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/photoLibrary/photoGalleryHeader";
|
||||
import { textStyles, viewStyles } from "../../styles/photoLibrary/photoGalleryHeader";
|
||||
import TranslatedText from "../SharedComponents/TranslatedText";
|
||||
|
||||
type Props = {
|
||||
@@ -24,10 +24,12 @@ const GroupPhotosHeader = ( { photos, observations }: Props ): Node => {
|
||||
return (
|
||||
<>
|
||||
<View style={viewStyles.header}>
|
||||
<HeaderBackButton onPress={navBack} />
|
||||
<TranslatedText style={textStyles.header} text="Group-Photos" />
|
||||
<HeaderBackButton onPress={navBack} />
|
||||
<TranslatedText style={textStyles.header} text="Group-Photos" />
|
||||
</View>
|
||||
<Text style={textStyles.header}>{t( "X-photos-X-observations", { photoCount: photos, observationCount: observations } )}</Text>
|
||||
<Text style={textStyles.header}>
|
||||
{t( "X-photos-X-observations", { photoCount: photos, observationCount: observations } )}
|
||||
</Text>
|
||||
<TranslatedText style={textStyles.text} text="Combine-photos-onboarding" />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { Pressable, Image, FlatList, ActivityIndicator, View, Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import {
|
||||
ActivityIndicator, FlatList, Image, Pressable, Text, View
|
||||
} from "react-native";
|
||||
|
||||
import { imageStyles, viewStyles } from "../../styles/photoLibrary/photoGallery";
|
||||
import PhotoGalleryHeader from "./PhotoGalleryHeader";
|
||||
import { PhotoGalleryContext } from "../../providers/contexts";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import { imageStyles, viewStyles } from "../../styles/photoLibrary/photoGallery";
|
||||
import RoundGreenButton from "../SharedComponents/Buttons/RoundGreenButton";
|
||||
import ViewNoFooter from "../SharedComponents/ViewNoFooter";
|
||||
import PhotoGalleryHeader from "./PhotoGalleryHeader";
|
||||
|
||||
const options = {
|
||||
first: 28,
|
||||
@@ -63,13 +65,13 @@ const PhotoGallery = ( ): Node => {
|
||||
const photosByAlbum = photoGallery[selectedAlbum];
|
||||
const photosSelectedInAlbum = selectedPhotos[selectedAlbum] || [];
|
||||
|
||||
const updatePhotoGallery = ( rerenderFlatList ) => {
|
||||
const updatePhotoGallery = rerenderFlatList => {
|
||||
setPhotoGallery( {
|
||||
...photoGallery,
|
||||
// there might be a better way to do this, but adding this key forces the FlatList
|
||||
// to rerender anytime an image is unselected
|
||||
rerenderFlatList
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
const selectPhoto = ( isSelected, item ) => {
|
||||
@@ -126,9 +128,8 @@ const PhotoGallery = ( ): Node => {
|
||||
const renderEmptyList = ( ) => {
|
||||
if ( fetchingPhotos ) {
|
||||
return <ActivityIndicator />;
|
||||
} else {
|
||||
return <Text>{t( "No-photos-found" )}</Text>;
|
||||
}
|
||||
return <Text>{t( "No-photos-found" )}</Text>;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
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";
|
||||
import usePhotoAlbums from "./hooks/usePhotoAlbums";
|
||||
|
||||
type Props = {
|
||||
updateAlbum: Function
|
||||
@@ -17,7 +17,7 @@ type Props = {
|
||||
const PhotoGalleryHeader = ( { updateAlbum }: Props ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
|
||||
const changeAlbum = ( newAlbum ) => {
|
||||
const changeAlbum = newAlbum => {
|
||||
updateAlbum( newAlbum !== "All" ? newAlbum : null );
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
const sortByTime = array => array.sort( ( a, b ) => b.timestamp - a.timestamp );
|
||||
|
||||
const orderByTimestamp = ( albums: Array<Object>, selectedPhotos: Array<Object> ): Array<Object> => {
|
||||
const orderByTimestamp = (
|
||||
albums: Array<Object>,
|
||||
selectedPhotos: Array<Object>
|
||||
): Array<Object> => {
|
||||
let unorderedPhotos = [];
|
||||
albums.forEach( album => {
|
||||
unorderedPhotos = unorderedPhotos.concat( selectedPhotos[album] );
|
||||
@@ -12,11 +15,9 @@ const orderByTimestamp = ( albums: Array<Object>, selectedPhotos: Array<Object>
|
||||
const ordered = sortByTime( unorderedPhotos );
|
||||
|
||||
// nest under photos
|
||||
return ordered.map( photo => {
|
||||
return {
|
||||
photos: [photo]
|
||||
};
|
||||
} );
|
||||
return ordered.map( photo => ( {
|
||||
photos: [photo]
|
||||
} ) );
|
||||
};
|
||||
|
||||
const flattenAndOrderSelectedPhotos = ( selectedObservations: ?Array<Object> ): Array<Object> => {
|
||||
@@ -27,10 +28,10 @@ const flattenAndOrderSelectedPhotos = ( selectedObservations: ?Array<Object> ):
|
||||
} );
|
||||
|
||||
// sort selected observations by timestamp and avoid duplicates
|
||||
return [...new Set( sortByTime( combinedPhotos ) ) ];
|
||||
return [...new Set( sortByTime( combinedPhotos ) )];
|
||||
};
|
||||
|
||||
export {
|
||||
orderByTimestamp,
|
||||
flattenAndOrderSelectedPhotos
|
||||
flattenAndOrderSelectedPhotos,
|
||||
orderByTimestamp
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import CameraRoll from "@react-native-community/cameraroll";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const cameraRoll = [{
|
||||
label: "camera roll",
|
||||
@@ -31,7 +31,6 @@ const usePhotoAlbums = ( ): Array<Object> => {
|
||||
setPhotoAlbums( names );
|
||||
} catch ( e ) {
|
||||
console.log( e, "couldn't fetch photo albums" );
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,5 +45,3 @@ const usePhotoAlbums = ( ): Array<Object> => {
|
||||
};
|
||||
|
||||
export default usePhotoAlbums;
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import CameraRoll from "@react-native-community/cameraroll";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
// import uuid from "react-native-uuid";
|
||||
|
||||
// import { formatDateAndTime } from "../../../sharedHelpers/dateAndTime";
|
||||
@@ -24,14 +24,20 @@ const initialStatus = {
|
||||
* now, e.g. if permissions have been granted (Android), or if it's ok to
|
||||
* request permissions (iOS)
|
||||
*/
|
||||
const usePhotos = ( options: Object, isScrolling: boolean, canRequestPhotos: boolean = true ): Object => {
|
||||
const usePhotos = (
|
||||
options: Object,
|
||||
isScrolling: boolean,
|
||||
canRequestPhotos: boolean = true
|
||||
): Object => {
|
||||
const [photoFetchStatus, setPhotoFetchStatus] = useState( initialStatus );
|
||||
|
||||
const fetchPhotos = useCallback( async ( ) => {
|
||||
const { lastCursor, photos, fetchingPhotos, hasNextPage } = photoFetchStatus;
|
||||
const {
|
||||
lastCursor, photos, fetchingPhotos, hasNextPage
|
||||
} = photoFetchStatus;
|
||||
|
||||
const mapPhotoUris = ( p ) => p.edges.map( ( { node } ) => {
|
||||
return node;
|
||||
const mapPhotoUris = p => p.edges.map(
|
||||
( { node } ) => node
|
||||
// const latitude = node.location && node.location.latitude;
|
||||
// const longitude = node.location && node.location.longitude;
|
||||
// return {
|
||||
@@ -43,7 +49,7 @@ const usePhotos = ( options: Object, isScrolling: boolean, canRequestPhotos: boo
|
||||
// // adding a uuid here makes it easier to prevent duplicates in uploader
|
||||
// uuid: uuid.v4( )
|
||||
// };
|
||||
} );
|
||||
);
|
||||
|
||||
try {
|
||||
// keep track of the last photo fetched
|
||||
@@ -93,7 +99,7 @@ const usePhotos = ( options: Object, isScrolling: boolean, canRequestPhotos: boo
|
||||
const changedAlbum = ( ) => {
|
||||
if ( options.groupName ) {
|
||||
return photoFetchStatus.lastAlbum !== options.groupName;
|
||||
} else if ( !options.groupName && photoFetchStatus.lastAlbum ) {
|
||||
} if ( !options.groupName && photoFetchStatus.lastAlbum ) {
|
||||
// switch back to all photos mode
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import ViewWithFooter from "./SharedComponents/ViewWithFooter";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
|
||||
import PlaceholderText from "./PlaceholderText";
|
||||
import ViewWithFooter from "./SharedComponents/ViewWithFooter";
|
||||
|
||||
const PlaceholderComponent = ( ): Node => (
|
||||
<ViewWithFooter>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Text } from "react-native";
|
||||
import type { Node } from "react";
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
@@ -15,5 +15,4 @@ const PlaceholderText = ( { text, style }: Props ): Node => (
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
export default PlaceholderText;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { Text, Image, ImageBackground } from "react-native";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import * as React from "react";
|
||||
import { Image, ImageBackground, Text } from "react-native";
|
||||
|
||||
import { imageStyles, textStyles } from "../../styles/projects/projectDetails";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import { textStyles, imageStyles } from "../../styles/projects/projectDetails";
|
||||
import useProjectDetails from "./hooks/useProjectDetails";
|
||||
import ProjectObservations from "./ProjectObservations";
|
||||
|
||||
@@ -16,25 +16,24 @@ const ProjectDetails = ( ): React.Node => {
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<ImageBackground
|
||||
source={{ uri: project.header_image_url }}
|
||||
<ImageBackground
|
||||
source={{ uri: project.header_image_url }}
|
||||
// $FlowFixMe
|
||||
style={imageStyles.headerImage}
|
||||
testID="ProjectDetails.headerImage"
|
||||
>
|
||||
<Image
|
||||
source={{ uri: project.icon }}
|
||||
style={imageStyles.icon}
|
||||
testID="ProjectDetails.projectIcon"
|
||||
/>
|
||||
</ImageBackground>
|
||||
<Text style={textStyles.descriptionText}>{project.title}</Text>
|
||||
<Text style={textStyles.descriptionText}>{project.description}</Text>
|
||||
{/* TODO: support joining or leaving projects once oauth is set up */}
|
||||
<ProjectObservations id={id} />
|
||||
style={imageStyles.headerImage}
|
||||
testID="ProjectDetails.headerImage"
|
||||
>
|
||||
<Image
|
||||
source={{ uri: project.icon }}
|
||||
style={imageStyles.icon}
|
||||
testID="ProjectDetails.projectIcon"
|
||||
/>
|
||||
</ImageBackground>
|
||||
<Text style={textStyles.descriptionText}>{project.title}</Text>
|
||||
<Text style={textStyles.descriptionText}>{project.description}</Text>
|
||||
{/* TODO: support joining or leaving projects once oauth is set up */}
|
||||
<ProjectObservations id={id} />
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetails;
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { FlatList, Pressable, Text, Image } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as React from "react";
|
||||
import {
|
||||
FlatList, Image, Pressable, Text
|
||||
} from "react-native";
|
||||
|
||||
import { imageStyles, viewStyles, textStyles } from "../../styles/projects/projects";
|
||||
import { imageStyles, textStyles, viewStyles } from "../../styles/projects/projects";
|
||||
|
||||
type Props = {
|
||||
data: Array<Object>
|
||||
@@ -21,7 +23,11 @@ const ProjectList = ( { data }: Props ): React.Node => {
|
||||
style={viewStyles.row}
|
||||
testID={`Project.${item.id}`}
|
||||
>
|
||||
<Image source={{ uri: item.icon }} style={imageStyles.projectIcon} testID={`Project.${item.id}.photo`}/>
|
||||
<Image
|
||||
source={{ uri: item.icon }}
|
||||
style={imageStyles.projectIcon}
|
||||
testID={`Project.${item.id}.photo`}
|
||||
/>
|
||||
<Text style={textStyles.projectName}>{item.title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -37,4 +43,3 @@ const ProjectList = ( { data }: Props ): React.Node => {
|
||||
};
|
||||
|
||||
export default ProjectList;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as React from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
import useProjectObservations from "./hooks/useProjectObservations";
|
||||
import GridItem from "../SharedComponents/ObservationViews/GridItem";
|
||||
import useProjectObservations from "./hooks/useProjectObservations";
|
||||
|
||||
type Props = {
|
||||
id: number
|
||||
@@ -14,9 +14,13 @@ type Props = {
|
||||
const ProjectObservations = ( { id }: Props ): React.Node => {
|
||||
const observations = useProjectObservations( id );
|
||||
const navigation = useNavigation( );
|
||||
const navToObsDetails = observation => navigation.navigate( "ObsDetails", { uuid: observation.uuid } );
|
||||
const navToObsDetails = observation => {
|
||||
navigation.navigate( "ObsDetails", { uuid: observation.uuid } );
|
||||
};
|
||||
|
||||
const renderGridItem = ( { item } ) => <GridItem item={item} handlePress={navToObsDetails} uri="project" />;
|
||||
const renderGridItem = ( { item } ) => (
|
||||
<GridItem item={item} handlePress={navToObsDetails} uri="project" />
|
||||
);
|
||||
return (
|
||||
<FlatList
|
||||
data={observations}
|
||||
@@ -29,4 +33,3 @@ const ProjectObservations = ( { id }: Props ): React.Node => {
|
||||
};
|
||||
|
||||
export default ProjectObservations;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { t } from "i18next";
|
||||
import * as React from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { t } from "i18next";
|
||||
|
||||
import useUserLocation from "../../sharedHooks/useUserLocation";
|
||||
import { viewStyles } from "../../styles/projects/projects";
|
||||
import useMemberId from "./hooks/useMemberId";
|
||||
import useProjects from "./hooks/useProjects";
|
||||
import ProjectList from "./ProjectList";
|
||||
import { useUserLocation } from "../../sharedHooks/useUserLocation";
|
||||
import useMemberId from "./hooks/useMemberId";
|
||||
|
||||
const ProjectTabs = ( ): React.Node => {
|
||||
const memberId = useMemberId( );
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import ProjectSearch from "./ProjectSearch";
|
||||
import ProjectTabs from "./ProjectTabs";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
|
||||
const Projects = ( ): React.Node => {
|
||||
const [q, setQ] = React.useState( "" );
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getJWTToken } from "../../LoginSignUp/AuthenticationService";
|
||||
|
||||
const useMemberId = ( ): ?number => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const FIELDS = {
|
||||
title: true,
|
||||
@@ -26,7 +26,7 @@ const useProjectDetails = ( id: number ): Object => {
|
||||
setProjectDetails( results[0] );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( `Couldn't fetch project details for project_id ${id}:`, e.message, );
|
||||
console.log( `Couldn't fetch project details for project_id ${id}:`, e.message );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const PHOTO_FIELDS = {
|
||||
id: true,
|
||||
@@ -44,7 +44,7 @@ const useProjectObservations = ( id: number ): Object => {
|
||||
setProjectObservations( results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( `Couldn't fetch project observations for project_id ${id}:`, e.message, );
|
||||
console.log( `Couldn't fetch project observations for project_id ${id}:`, e.message );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const FIELDS = {
|
||||
title: true,
|
||||
@@ -28,7 +28,7 @@ const useProjects = ( apiParams: Object ): Array<Object> => {
|
||||
setProjects( results );
|
||||
} catch ( e ) {
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch projects:", e.message, );
|
||||
console.log( "Couldn't fetch projects:", e.message );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { FlatList, Pressable, Text, Image, View } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as React from "react";
|
||||
import {
|
||||
FlatList, Image, Pressable, Text, View
|
||||
} from "react-native";
|
||||
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import { viewStyles, imageStyles } from "../../styles/search/search";
|
||||
import { imageStyles, viewStyles } from "../../styles/search/search";
|
||||
import PlaceholderText from "../PlaceholderText";
|
||||
import InputField from "../SharedComponents/InputField";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
|
||||
const Search = ( ): React.Node => {
|
||||
const navigation = useNavigation( );
|
||||
@@ -30,23 +32,30 @@ const Search = ( ): React.Node => {
|
||||
style={viewStyles.row}
|
||||
testID={`Search.taxa.${item.id}`}
|
||||
>
|
||||
<Image source={imageUrl} style={imageStyles.squareImage} testID={`Search.${item.id}.photo`} />
|
||||
<Image
|
||||
source={imageUrl}
|
||||
style={imageStyles.squareImage}
|
||||
testID={`Search.${item.id}.photo`}
|
||||
/>
|
||||
<Text>{`${item.preferred_common_name} (${item.rank} ${item.name})`}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={navToUserProfile}
|
||||
style={viewStyles.row}
|
||||
testID={`Search.user.${item.login}`}
|
||||
>
|
||||
{/* TODO: add an empty icon when user doesn't have an icon */}
|
||||
<Image source={{ uri: item.icon }} style={imageStyles.circularImage} testID={`Search.${item.login}.photo`}/>
|
||||
<Text>{`${item.login} (${item.name})`}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Pressable
|
||||
onPress={navToUserProfile}
|
||||
style={viewStyles.row}
|
||||
testID={`Search.user.${item.login}`}
|
||||
>
|
||||
{/* TODO: add an empty icon when user doesn't have an icon */}
|
||||
<Image
|
||||
source={{ uri: item.icon }}
|
||||
style={imageStyles.circularImage}
|
||||
testID={`Search.${item.login}.photo`}
|
||||
/>
|
||||
<Text>{`${item.login} (${item.name})`}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
const setTaxaSearch = ( ) => setQueryType( "taxa" );
|
||||
@@ -76,11 +85,11 @@ const Search = ( ): React.Node => {
|
||||
text={q}
|
||||
type="none"
|
||||
/>
|
||||
<FlatList
|
||||
data={list}
|
||||
renderItem={renderItem}
|
||||
testID="Search.listView"
|
||||
/>
|
||||
<FlatList
|
||||
data={list}
|
||||
renderItem={renderItem}
|
||||
testID="Search.listView"
|
||||
/>
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
39
src/components/Settings/BlockedUser.js
Normal file
39
src/components/Settings/BlockedUser.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// @flow
|
||||
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
Text,
|
||||
View
|
||||
} from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/settings/settings";
|
||||
|
||||
type Props = {
|
||||
unblockUser: Function,
|
||||
user: Object
|
||||
}
|
||||
|
||||
const BlockedUser = ( { user, unblockUser }:Props ): Node => (
|
||||
<View style={[viewStyles.row, viewStyles.relationshipRow]}>
|
||||
<Image
|
||||
style={viewStyles.relationshipImage}
|
||||
source={{ uri: user.icon }}
|
||||
/>
|
||||
<View style={viewStyles.column}>
|
||||
<Text>{user.login}</Text>
|
||||
<Text>{user.name}</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
style={viewStyles.removeRelationship}
|
||||
onPress={() => unblockUser( user )}
|
||||
>
|
||||
<Text>{t( "Unblock" )}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default BlockedUser;
|
||||
39
src/components/Settings/MutedUser.js
Normal file
39
src/components/Settings/MutedUser.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// @flow
|
||||
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
Text,
|
||||
View
|
||||
} from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/settings/settings";
|
||||
|
||||
type Props = {
|
||||
unmuteUser: Function,
|
||||
user: Object
|
||||
}
|
||||
|
||||
const MutedUser = ( { user, unmuteUser }: Props ): Node => (
|
||||
<View style={[viewStyles.row, viewStyles.relationshipRow]}>
|
||||
<Image
|
||||
style={viewStyles.relationshipImage}
|
||||
source={{ uri: user.icon }}
|
||||
/>
|
||||
<View style={viewStyles.column}>
|
||||
<Text>{user.login}</Text>
|
||||
<Text>{user.name}</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
style={viewStyles.removeRelationship}
|
||||
onPress={() => unmuteUser( user )}
|
||||
>
|
||||
<Text>{t( "Unmute" )}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default MutedUser;
|
||||
@@ -1,13 +1,16 @@
|
||||
import React, {useEffect} from "react";
|
||||
import {useDebounce} from "use-debounce";
|
||||
import {Image, Text, TextInput, View} from "react-native";
|
||||
import {textStyles, viewStyles} from "../../styles/settings/settings";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Image, Text, TextInput, View
|
||||
} from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import {inatPlaceTypes} from "../../dictionaries/places";
|
||||
import usePlaces from "./hooks/usePlaces";
|
||||
import usePlaceDetails from "./hooks/usePlaceDetails";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
const PlaceSearchInput = ( { placeId, onPlaceChanged} ): React.Node => {
|
||||
import inatPlaceTypes from "../../dictionaries/places";
|
||||
import { textStyles, viewStyles } from "../../styles/settings/settings";
|
||||
import usePlaceDetails from "./hooks/usePlaceDetails";
|
||||
import usePlaces from "./hooks/usePlaces";
|
||||
|
||||
const PlaceSearchInput = ( { placeId, onPlaceChanged } ): React.Node => {
|
||||
const [hideResults, setHideResults] = React.useState( true );
|
||||
const [placeSearch, setPlaceSearch] = React.useState( "" );
|
||||
// So we'll start searching only once the user finished typing
|
||||
@@ -22,25 +25,27 @@ const PlaceSearchInput = ( { placeId, onPlaceChanged} ): React.Node => {
|
||||
} else {
|
||||
setPlaceSearch( "" );
|
||||
}
|
||||
|
||||
}, [placeDetails] );
|
||||
|
||||
return (
|
||||
return (
|
||||
<View style={viewStyles.column}>
|
||||
<View style={viewStyles.row}>
|
||||
<TextInput
|
||||
style={viewStyles.textInput}
|
||||
onChangeText={( v ) => {
|
||||
onChangeText={v => {
|
||||
setHideResults( false );
|
||||
setPlaceSearch( v );
|
||||
}}
|
||||
value={placeSearch}
|
||||
/>
|
||||
<Pressable style={viewStyles.clearSearch} onPress={() => {
|
||||
setHideResults( true );
|
||||
setPlaceSearch( "" );
|
||||
onPlaceChanged( 0 );
|
||||
}}>
|
||||
<Pressable
|
||||
style={viewStyles.clearSearch}
|
||||
onPress={() => {
|
||||
setHideResults( true );
|
||||
setPlaceSearch( "" );
|
||||
onPlaceChanged( 0 );
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={viewStyles.clearSearch}
|
||||
resizeMode="contain"
|
||||
@@ -48,19 +53,21 @@ const PlaceSearchInput = ( { placeId, onPlaceChanged} ): React.Node => {
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
{!hideResults && finalPlaceSearch.length > 0 && placeResults.map( ( place ) => (
|
||||
<Pressable key={place.id} style={[viewStyles.row, viewStyles.placeResultContainer]}
|
||||
onPress={() => {
|
||||
setHideResults( true );
|
||||
onPlaceChanged( place.id );
|
||||
}}>
|
||||
{!hideResults && finalPlaceSearch.length > 0 && placeResults.map( place => (
|
||||
<Pressable
|
||||
key={place.id}
|
||||
style={[viewStyles.row, viewStyles.placeResultContainer]}
|
||||
onPress={() => {
|
||||
setHideResults( true );
|
||||
onPlaceChanged( place.id );
|
||||
}}
|
||||
>
|
||||
<Text style={textStyles.resultPlaceName}>{place.display_name}</Text>
|
||||
<Text style={textStyles.resultPlaceType}>{inatPlaceTypes[place.place_type]}</Text>
|
||||
</Pressable>
|
||||
) )}
|
||||
</View>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default PlaceSearchInput;
|
||||
|
||||
71
src/components/Settings/Relationship.js
Normal file
71
src/components/Settings/Relationship.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// @flow
|
||||
|
||||
import CheckBox from "@react-native-community/checkbox";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
Text,
|
||||
View
|
||||
} from "react-native";
|
||||
|
||||
import colors from "../../styles/colors";
|
||||
import { viewStyles } from "../../styles/settings/settings";
|
||||
|
||||
type Props = {
|
||||
relationship: Object,
|
||||
updateRelationship: Function,
|
||||
askToRemoveRelationship: Function
|
||||
}
|
||||
|
||||
const Relationship = ( {
|
||||
askToRemoveRelationship,
|
||||
relationship,
|
||||
updateRelationship
|
||||
}: Props ): Node => (
|
||||
<View style={[viewStyles.column, viewStyles.relationshipRow]}>
|
||||
<View style={viewStyles.row}>
|
||||
<Image
|
||||
style={viewStyles.relationshipImage}
|
||||
source={{ uri: relationship.friendUser.icon_url }}
|
||||
/>
|
||||
<View style={viewStyles.column}>
|
||||
<Text>{relationship.friendUser.login}</Text>
|
||||
<Text>{relationship.friendUser.name}</Text>
|
||||
</View>
|
||||
<View style={viewStyles.column}>
|
||||
<View style={[viewStyles.row, viewStyles.notificationCheckbox]}>
|
||||
<CheckBox
|
||||
value={relationship.following}
|
||||
onValueChange={
|
||||
( ) => { updateRelationship( relationship, { following: !relationship.following } ); }
|
||||
}
|
||||
tintColors={{ false: colors.inatGreen, true: colors.inatGreen }}
|
||||
/>
|
||||
<Text>{t( "Following" )}</Text>
|
||||
</View>
|
||||
<View style={[viewStyles.row, viewStyles.notificationCheckbox]}>
|
||||
<CheckBox
|
||||
value={relationship.trust}
|
||||
onValueChange={
|
||||
( ) => { updateRelationship( relationship, { trust: !relationship.trust } ); }
|
||||
}
|
||||
tintColors={{ false: colors.inatGreen, true: colors.inatGreen }}
|
||||
/>
|
||||
<Text>{t( "Trust-with-hidden-coordinates" )}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text>{t( "Added-on-date", { date: relationship.created_at } )}</Text>
|
||||
<Pressable
|
||||
style={viewStyles.removeRelationship}
|
||||
onPress={() => askToRemoveRelationship( relationship )}
|
||||
>
|
||||
<Text>{t( "Remove-Relationship" )}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default Relationship;
|
||||
@@ -1,7 +1,11 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { t } from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator, Alert,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Button,
|
||||
Pressable,
|
||||
SafeAreaView,
|
||||
@@ -10,22 +14,20 @@ import {
|
||||
Text,
|
||||
View
|
||||
} from "react-native";
|
||||
import type { Node } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { viewStyles, textStyles } from "../../styles/settings/settings";
|
||||
|
||||
import { textStyles, viewStyles } from "../../styles/settings/settings";
|
||||
import { getAPIToken } from "../LoginSignUp/AuthenticationService";
|
||||
import SettingsProfile from "./SettingsProfile";
|
||||
import {
|
||||
SettingsNotifications,
|
||||
EMAIL_NOTIFICATIONS
|
||||
} from "./SettingsNotifications";
|
||||
import SettingsAccount from "./SettingsAccount";
|
||||
import SettingsContentDisplay from "./SettingsContentDisplay";
|
||||
import SettingsApplications from "./SettingsApplications";
|
||||
import SettingsRelationships from "./SettingsRelationships";
|
||||
import ViewWithFooter from "../SharedComponents/ViewWithFooter";
|
||||
import useUserMe from "./hooks/useUserMe";
|
||||
import { t } from "i18next";
|
||||
import SettingsAccount from "./SettingsAccount";
|
||||
import SettingsApplications from "./SettingsApplications";
|
||||
import SettingsContentDisplay from "./SettingsContentDisplay";
|
||||
import {
|
||||
EMAIL_NOTIFICATIONS,
|
||||
SettingsNotifications
|
||||
} from "./SettingsNotifications";
|
||||
import SettingsProfile from "./SettingsProfile";
|
||||
import SettingsRelationships from "./SettingsRelationships";
|
||||
|
||||
const TAB_TYPE_PROFILE = "profile";
|
||||
const TAB_TYPE_ACCOUNT = "account";
|
||||
@@ -69,86 +71,82 @@ type Props = {
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
const SettingsTabs = ( { activeTab, onTabPress } ): React.Node => {
|
||||
return (
|
||||
<>
|
||||
<View style={[viewStyles.tabsRow, viewStyles.shadow]}>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_PROFILE )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={activeTab === TAB_TYPE_PROFILE ? textStyles.activeTab : null}
|
||||
>
|
||||
{t( "Profile" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_ACCOUNT )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={activeTab === TAB_TYPE_ACCOUNT ? textStyles.activeTab : null}
|
||||
>
|
||||
{t( "Account" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_NOTIFICATIONS )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
const SettingsTabs = ( { activeTab, onTabPress } ): React.Node => (
|
||||
<View style={[viewStyles.tabsRow, viewStyles.shadow]}>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_PROFILE )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={activeTab === TAB_TYPE_PROFILE ? textStyles.activeTab : null}
|
||||
>
|
||||
{t( "Profile" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_ACCOUNT )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={activeTab === TAB_TYPE_ACCOUNT ? textStyles.activeTab : null}
|
||||
>
|
||||
{t( "Account" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_NOTIFICATIONS )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
activeTab === TAB_TYPE_NOTIFICATIONS ? textStyles.activeTab : null
|
||||
}
|
||||
>
|
||||
{t( "Notifications" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_RELATIONSHIPS )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
>
|
||||
{t( "Notifications" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_RELATIONSHIPS )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
activeTab === TAB_TYPE_RELATIONSHIPS ? textStyles.activeTab : null
|
||||
}
|
||||
>
|
||||
{t( "Relationships" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_CONTENT_DISPLAY )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
>
|
||||
{t( "Relationships" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_CONTENT_DISPLAY )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
activeTab === TAB_TYPE_CONTENT_DISPLAY
|
||||
? textStyles.activeTab
|
||||
: null
|
||||
}
|
||||
>
|
||||
{t( "Content-Display" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_APPLICATIONS )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
>
|
||||
{t( "Content-Display" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => onTabPress( TAB_TYPE_APPLICATIONS )}
|
||||
accessibilityRole="link"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
activeTab === TAB_TYPE_APPLICATIONS ? textStyles.activeTab : null
|
||||
}
|
||||
>
|
||||
{t( "Applications" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
>
|
||||
{t( "Applications" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
const Settings = ( { children }: Props ): Node => {
|
||||
const Settings = ( { children: _children }: Props ): Node => {
|
||||
const [activeTab, setActiveTab] = useState( TAB_TYPE_PROFILE );
|
||||
const [settings, setSettings] = useState( {} );
|
||||
const [accessToken, setAccessToken] = useState( null );
|
||||
@@ -223,12 +221,13 @@ const Settings = ( { children }: Props ): Node => {
|
||||
useFocusEffect(
|
||||
React.useCallback( () => {
|
||||
// Reload the settings
|
||||
getAPIToken( true ).then( ( token ) => {
|
||||
getAPIToken( true ).then( token => {
|
||||
setAccessToken( token );
|
||||
} );
|
||||
|
||||
return () => {
|
||||
// De-focused - clean up the access token (this will force a refresh later when we're re-focused)
|
||||
// De-focused - clean up the access token (this will force a refresh
|
||||
// later when we're re-focused)
|
||||
setAccessToken( null );
|
||||
};
|
||||
}, [] )
|
||||
|
||||
@@ -1,82 +1,92 @@
|
||||
// @flow
|
||||
|
||||
import {Text, View} from "react-native";
|
||||
import {viewStyles, textStyles} from "../../styles/settings/settings";
|
||||
import React from "react";
|
||||
import {Picker} from "@react-native-picker/picker";
|
||||
import {colors} from "../../styles/global";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import CheckBox from "@react-native-community/checkbox";
|
||||
import {inatLanguages} from "../../dictionaries/languages";
|
||||
import {inatNetworks} from "../../dictionaries/networks";
|
||||
import PlaceSearchInput from "./PlaceSearchInput";
|
||||
import type { Node } from "react";
|
||||
import type { SettingsProps } from "./types";
|
||||
import { Picker } from "@react-native-picker/picker";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
|
||||
const SettingsAccount = ( { settings, onSettingsModified }: SettingsProps ): Node => {
|
||||
import inatLanguages from "../../dictionaries/languages";
|
||||
import inatNetworks from "../../dictionaries/networks";
|
||||
import colors from "../../styles/colors";
|
||||
import { textStyles, viewStyles } from "../../styles/settings/settings";
|
||||
import PlaceSearchInput from "./PlaceSearchInput";
|
||||
import type { SettingsProps } from "./types";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={textStyles.title}>{t( "Account" )}</Text>
|
||||
const SettingsAccount = ( { settings, onSettingsModified }: SettingsProps ): Node => (
|
||||
<>
|
||||
<Text style={textStyles.title}>{t( "Account" )}</Text>
|
||||
|
||||
<Text style={[textStyles.subTitle]}>{t( "Language-Locale" )}</Text>
|
||||
<Text>{t( "This-sets-your-language-and-date-formatting-preferences-across-iNaturalist" )}</Text>
|
||||
<View style={viewStyles.selectorContainer}>
|
||||
<Picker
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={settings.locale}
|
||||
onValueChange={( itemValue, itemIndex ) =>
|
||||
onSettingsModified( { ...settings, locale: itemValue } )
|
||||
}>
|
||||
{Object.keys( inatLanguages ).map( ( k ) => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={inatLanguages[k]}
|
||||
value={k} />
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
<Text style={[textStyles.subTitle]}>{t( "Language-Locale" )}</Text>
|
||||
<Text>{t( "This-sets-your-language-and-date-formatting-preferences-across-iNaturalist" )}</Text>
|
||||
<View style={viewStyles.selectorContainer}>
|
||||
<Picker
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={settings.locale}
|
||||
onValueChange={
|
||||
( itemValue, _itemIndex ) => onSettingsModified( { ...settings, locale: itemValue } )
|
||||
}
|
||||
>
|
||||
{Object.keys( inatLanguages ).map( k => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={inatLanguages[k]}
|
||||
value={k}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<Text style={[textStyles.subTitle]}>{t( "Default-Search-Place" )}</Text>
|
||||
<Text>{t( "This-will-be-your-default-place-for-all-searches-in-Explore-and-Identify" )}</Text>
|
||||
<PlaceSearchInput placeId={settings.search_place_id} onPlaceChanged={( p ) => onSettingsModified( { ...settings, search_place_id: p} )} />
|
||||
<Text style={[textStyles.subTitle]}>{t( "Default-Search-Place" )}</Text>
|
||||
<Text>{t( "This-will-be-your-default-place-for-all-searches-in-Explore-and-Identify" )}</Text>
|
||||
<PlaceSearchInput
|
||||
placeId={settings.search_place_id}
|
||||
onPlaceChanged={p => onSettingsModified( { ...settings, search_place_id: p } )}
|
||||
/>
|
||||
|
||||
<Text style={[textStyles.subTitle]}>{t( "Privacy" )}</Text>
|
||||
<Pressable style={[viewStyles.row, viewStyles.notificationCheckbox]}
|
||||
onPress={() => onSettingsModified( { ...settings, prefers_no_tracking: !settings.prefers_no_tracking} )}>
|
||||
<CheckBox
|
||||
value={settings.prefers_no_tracking}
|
||||
onValueChange={( v ) => onSettingsModified( { ...settings, prefers_no_tracking: v} )}
|
||||
tintColors={{false: colors.inatGreen, true: colors.inatGreen}}
|
||||
/>
|
||||
<Text style={[textStyles.checkbox, viewStyles.column]}>{t( "Do-not-collect-stability-and-usage-data-using-third-party-services" )}</Text>
|
||||
</Pressable>
|
||||
<Text style={[textStyles.subTitle]}>{t( "Privacy" )}</Text>
|
||||
<Pressable
|
||||
style={[viewStyles.row, viewStyles.notificationCheckbox]}
|
||||
onPress={() => onSettingsModified( {
|
||||
...settings,
|
||||
prefers_no_tracking: !settings.prefers_no_tracking
|
||||
} )}
|
||||
>
|
||||
<CheckBox
|
||||
value={settings.prefers_no_tracking}
|
||||
onValueChange={v => onSettingsModified( { ...settings, prefers_no_tracking: v } )}
|
||||
tintColors={{ false: colors.inatGreen, true: colors.inatGreen }}
|
||||
/>
|
||||
<Text style={[textStyles.checkbox, viewStyles.column]}>
|
||||
{t( "Do-not-collect-stability-and-usage-data-using-third-party-services" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text style={[textStyles.subTitle]}>{t( "iNaturalist-Network-Affiliation" )}</Text>
|
||||
<View style={viewStyles.selectorContainer}>
|
||||
<Picker
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={settings.site_id}
|
||||
onValueChange={
|
||||
( itemValue, _itemIndex ) => onSettingsModified( { ...settings, site_id: itemValue } )
|
||||
}
|
||||
>
|
||||
{Object.keys( inatNetworks ).map( k => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={inatNetworks[k].name}
|
||||
value={k}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
<Text>{t( "The-iNaturalist-Network-is-a-collection-of-localized-websites" )}</Text>
|
||||
|
||||
<Text style={[textStyles.subTitle]}>{t( "iNaturalist-Network-Affiliation" )}</Text>
|
||||
<View style={viewStyles.selectorContainer}>
|
||||
<Picker
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={settings.site_id}
|
||||
onValueChange={( itemValue, itemIndex ) =>
|
||||
onSettingsModified( { ...settings, site_id: itemValue } )
|
||||
}>
|
||||
{Object.keys( inatNetworks ).map( ( k ) => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={inatNetworks[k].name}
|
||||
value={k} />
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
<Text>{t( "The-iNaturalist-Network-is-a-collection-of-localized-websites" )}</Text>
|
||||
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
</>
|
||||
);
|
||||
|
||||
export default SettingsAccount;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import {Alert, Text, View} from "react-native";
|
||||
import {viewStyles, textStyles} from "../../styles/settings/settings";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import type { Node } from "react";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import {inatProviders} from "../../dictionaries/providers";
|
||||
import { t } from "i18next";
|
||||
import inatjs from "inaturalistjs";
|
||||
import type { Node } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Alert, Text, View } from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
|
||||
import inatProviders from "../../dictionaries/providers";
|
||||
import { textStyles, viewStyles } from "../../styles/settings/settings";
|
||||
import useAuthorizedApplications from "./hooks/useAuthorizedApplications";
|
||||
import useProviderAuthorizations from "./hooks/useProviderAuthorizations";
|
||||
import { t } from "i18next";
|
||||
|
||||
type Props = {
|
||||
accessToken: string
|
||||
@@ -24,17 +25,19 @@ const SettingsApplications = ( { accessToken }: Props ): Node => {
|
||||
setAuthorizedApps( currentAuthorizedApps );
|
||||
}, [currentAuthorizedApps] );
|
||||
|
||||
const revokeApp = async ( appId ) => {
|
||||
const response = await inatjs.authorized_applications.delete( { id: appId }, {api_token: accessToken} );
|
||||
const revokeApp = async appId => {
|
||||
const response = await inatjs.authorized_applications.delete(
|
||||
{ id: appId },
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
console.log( "Revoked app", response );
|
||||
// Refresh authorized applications
|
||||
const apps = await inatjs.authorized_applications.search( {}, {api_token: accessToken} );
|
||||
const apps = await inatjs.authorized_applications.search( {}, { api_token: accessToken } );
|
||||
console.log( "Authorized Applications", apps.results );
|
||||
setAuthorizedApps( apps.results );
|
||||
};
|
||||
|
||||
|
||||
const askToRevokeApp = ( app ) => {
|
||||
const askToRevokeApp = app => {
|
||||
Alert.alert(
|
||||
`Revoke ${app.application.name}?`,
|
||||
"This will sign you out of your current session on this application.",
|
||||
@@ -50,23 +53,37 @@ const SettingsApplications = ( { accessToken }: Props ): Node => {
|
||||
return (
|
||||
<View style={viewStyles.column}>
|
||||
<Text style={textStyles.title}>{t( "iNaturalist-Applications" )}</Text>
|
||||
{authorizedApps.filter( ( app ) => app.application.official ).map( ( app ) => (
|
||||
<Text key={app.application.id}>{t( "authorized-on-date", { appName: app.application.name, date: app.created_at } )}</Text>
|
||||
{authorizedApps.filter( app => app.application.official ).map( app => (
|
||||
<Text key={app.application.id}>
|
||||
{t( "authorized-on-date", { appName: app.application.name, date: app.created_at } )}
|
||||
</Text>
|
||||
) )}
|
||||
|
||||
<Text style={[textStyles.title, textStyles.marginTop]}>{t( "Connected-Accounts" )}</Text>
|
||||
{Object.keys( inatProviders ).map( ( providerKey ) => {
|
||||
const connectedProvider = providerAuthorizations.find( x => x.provider_name === providerKey );
|
||||
return ( <Text
|
||||
key={providerKey}>{inatProviders[providerKey]} {connectedProvider && `(authorized on: ${connectedProvider.created_at})`}</Text> );
|
||||
{Object.keys( inatProviders ).map( providerKey => {
|
||||
const connectedProvider = providerAuthorizations.find(
|
||||
x => x.provider_name === providerKey
|
||||
);
|
||||
return (
|
||||
<Text
|
||||
key={providerKey}
|
||||
>
|
||||
{inatProviders[providerKey]}
|
||||
{" "}
|
||||
{connectedProvider && `(authorized on: ${connectedProvider.created_at})`}
|
||||
</Text>
|
||||
);
|
||||
} )}
|
||||
|
||||
|
||||
<Text style={[textStyles.title, textStyles.marginTop]}>{t( "External-Applications" )}</Text>
|
||||
{authorizedApps.filter( ( app ) => !app.application.official ).map( ( app ) => (
|
||||
{authorizedApps.filter( app => !app.application.official ).map( app => (
|
||||
<View key={app.application.id} style={[viewStyles.row, viewStyles.applicationRow]}>
|
||||
<Text style={textStyles.applicationName}>{t( "authorized-on-date", { appName: app.application.name, date: app.created_at } )}</Text>
|
||||
<Pressable style={viewStyles.revokeAccess} onPress={() => askToRevokeApp( app )}><Text>{t( "Revoke" )}</Text></Pressable>
|
||||
<Text style={textStyles.applicationName}>
|
||||
{t( "authorized-on-date", { appName: app.application.name, date: app.created_at } )}
|
||||
</Text>
|
||||
<Pressable style={viewStyles.revokeAccess} onPress={() => askToRevokeApp( app )}>
|
||||
<Text>{t( "Revoke" )}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) )}
|
||||
</View>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// @flow
|
||||
|
||||
import {Pressable, Text, View} from "react-native";
|
||||
import {viewStyles, textStyles} from "../../styles/settings/settings";
|
||||
import React from "react";
|
||||
import {Picker} from "@react-native-picker/picker";
|
||||
import {colors} from "../../styles/global";
|
||||
import CheckBox from "@react-native-community/checkbox";
|
||||
import PlaceSearchInput from "./PlaceSearchInput";
|
||||
import {inatLicenses} from "../../dictionaries/licenses";
|
||||
import type { Node } from "react";
|
||||
import type { SettingsProps } from "./types";
|
||||
import { Picker } from "@react-native-picker/picker";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
|
||||
import inatLicenses from "../../dictionaries/licenses";
|
||||
import colors from "../../styles/colors";
|
||||
import { textStyles, viewStyles } from "../../styles/settings/settings";
|
||||
import PlaceSearchInput from "./PlaceSearchInput";
|
||||
import type { SettingsProps } from "./types";
|
||||
|
||||
const PROJECT_SETTINGS = {
|
||||
any: "Any",
|
||||
@@ -30,7 +31,6 @@ const ADD_OBSERVATION_FIELDS = {
|
||||
observer: "Only you"
|
||||
};
|
||||
|
||||
|
||||
const LicenseSelector = ( {
|
||||
value,
|
||||
onValueChanged,
|
||||
@@ -38,73 +38,96 @@ const LicenseSelector = ( {
|
||||
updateExistingTitle,
|
||||
onUpdateExisting,
|
||||
updateExisting
|
||||
} ): Node => {
|
||||
return <>
|
||||
} ): Node => (
|
||||
<>
|
||||
<Text style={textStyles.subTitle}>{title}</Text>
|
||||
<View style={viewStyles.selectorContainer}>
|
||||
<Picker
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={value}
|
||||
onValueChange={onValueChanged}>
|
||||
{inatLicenses.map( ( l ) => (
|
||||
onValueChange={onValueChanged}
|
||||
>
|
||||
{inatLicenses.map( l => (
|
||||
<Picker.Item
|
||||
key={l.value}
|
||||
label={l.title}
|
||||
value={l.value} />
|
||||
value={l.value}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<Pressable style={[viewStyles.row, viewStyles.notificationCheckbox]} onPress={() => {
|
||||
onUpdateExisting( !updateExisting );
|
||||
}}>
|
||||
<Pressable
|
||||
style={[viewStyles.row, viewStyles.notificationCheckbox]}
|
||||
onPress={() => {
|
||||
onUpdateExisting( !updateExisting );
|
||||
}}
|
||||
>
|
||||
<CheckBox
|
||||
value={updateExisting}
|
||||
onValueChange={onUpdateExisting}
|
||||
tintColors={{false: colors.inatGreen, true: colors.inatGreen}}
|
||||
tintColors={{ false: colors.inatGreen, true: colors.inatGreen }}
|
||||
/>
|
||||
<Text style={textStyles.notificationTitle}>{updateExistingTitle}</Text>
|
||||
</Pressable>
|
||||
|
||||
</>;
|
||||
};
|
||||
</>
|
||||
);
|
||||
|
||||
const SettingsContentDisplay = ( { settings, onSettingsModified }: SettingsProps ): Node => {
|
||||
let taxonNamePreference = "prefers_common_names";
|
||||
if ( settings.prefers_scientific_name_first ) {
|
||||
taxonNamePreference = "prefers_scientific_name_first";
|
||||
} else if ( settings.prefers_scientific_names ) {
|
||||
taxonNamePreference = "prefers_scientific_names";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={textStyles.title}>{t( "Project-Settings" )}</Text>
|
||||
<Text style={textStyles.subTitle}>{t( "Which-traditional-projects-can-add-your-observations" )}</Text>
|
||||
<Text style={textStyles.subTitle}>
|
||||
{t( "Which-traditional-projects-can-add-your-observations" )}
|
||||
</Text>
|
||||
<View style={viewStyles.selectorContainer}>
|
||||
<Picker
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={settings.preferred_project_addition_by}
|
||||
onValueChange={( itemValue, itemIndex ) =>
|
||||
onSettingsModified( { ...settings, preferred_project_addition_by: itemValue } )
|
||||
}>
|
||||
{Object.keys( PROJECT_SETTINGS ).map( ( k ) => (
|
||||
onValueChange={( itemValue, _itemIndex ) => onSettingsModified( {
|
||||
...settings,
|
||||
preferred_project_addition_by: itemValue
|
||||
} )}
|
||||
>
|
||||
{Object.keys( PROJECT_SETTINGS ).map( k => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={PROJECT_SETTINGS[k]}
|
||||
value={k} />
|
||||
value={k}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<Text style={[textStyles.title, textStyles.marginTop]}>{t( "Taxonomy-Settings" )}</Text>
|
||||
<Pressable style={[viewStyles.row, viewStyles.notificationCheckbox]} onPress={() => {
|
||||
onSettingsModified( { ...settings, prefers_automatic_taxonomic_changes: !settings.prefers_automatic_taxonomic_changes } );
|
||||
}}>
|
||||
<Pressable
|
||||
style={[viewStyles.row, viewStyles.notificationCheckbox]}
|
||||
onPress={() => {
|
||||
onSettingsModified( {
|
||||
...settings,
|
||||
prefers_automatic_taxonomic_changes: !settings.prefers_automatic_taxonomic_changes
|
||||
} );
|
||||
}}
|
||||
>
|
||||
<CheckBox
|
||||
value={settings.prefers_automatic_taxonomic_changes}
|
||||
onValueChange={( v ) => {
|
||||
onValueChange={v => {
|
||||
onSettingsModified( { ...settings, prefers_automatic_taxonomic_changes: v } );
|
||||
}}
|
||||
tintColors={{false: colors.inatGreen, true: colors.inatGreen}}
|
||||
tintColors={{ false: colors.inatGreen, true: colors.inatGreen }}
|
||||
/>
|
||||
<Text style={textStyles.notificationTitle}>{t( "Automatically-update-my-content-for-taxon-changes" )}</Text>
|
||||
<Text style={textStyles.notificationTitle}>
|
||||
{t( "Automatically-update-my-content-for-taxon-changes" )}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text style={[textStyles.title, textStyles.marginTop]}>{t( "Names" )}</Text>
|
||||
@@ -114,68 +137,84 @@ const SettingsContentDisplay = ( { settings, onSettingsModified }: SettingsProps
|
||||
<Picker
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={
|
||||
settings.prefers_common_names && !settings.prefers_scientific_name_first ? "prefers_common_names" :
|
||||
( settings.prefers_common_names && settings.prefers_scientific_name_first ?
|
||||
"prefers_scientific_name_first" : "prefers_scientific_names"
|
||||
)
|
||||
}
|
||||
onValueChange={( value, itemIndex ) => {
|
||||
selectedValue={taxonNamePreference}
|
||||
onValueChange={( value, _itemIndex ) => {
|
||||
if ( value === "prefers_common_names" ) {
|
||||
onSettingsModified( { ...settings,
|
||||
onSettingsModified( {
|
||||
...settings,
|
||||
prefers_common_names: true,
|
||||
prefers_scientific_name_first: false
|
||||
} );
|
||||
} else if ( value === "prefers_scientific_name_first" ) {
|
||||
onSettingsModified( { ...settings,
|
||||
onSettingsModified( {
|
||||
...settings,
|
||||
prefers_common_names: true,
|
||||
prefers_scientific_name_first: true
|
||||
} );
|
||||
} else if ( value === "prefers_scientific_names" ) {
|
||||
onSettingsModified( { ...settings,
|
||||
onSettingsModified( {
|
||||
...settings,
|
||||
prefers_common_names: false,
|
||||
prefers_scientific_name_first: false
|
||||
} );
|
||||
}
|
||||
}}>
|
||||
{Object.keys( TAXON_DISPLAY ).map( ( k ) => (
|
||||
}}
|
||||
>
|
||||
{Object.keys( TAXON_DISPLAY ).map( k => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={TAXON_DISPLAY[k]}
|
||||
value={k} />
|
||||
value={k}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
<Text style={textStyles.subTitle}>{t( "Prioritize-common-names-used-in-this-place" )}</Text>
|
||||
<PlaceSearchInput placeId={settings.place_id} onPlaceChanged={( p ) => onSettingsModified( { ...settings, place_id: p} )} />
|
||||
<PlaceSearchInput
|
||||
placeId={settings.place_id}
|
||||
onPlaceChanged={p => onSettingsModified( { ...settings, place_id: p } )}
|
||||
/>
|
||||
|
||||
<Text style={[textStyles.title, textStyles.marginTop]}>{t( "Community-Moderation-Settings" )}</Text>
|
||||
<Pressable style={[viewStyles.row, viewStyles.notificationCheckbox]} onPress={() => {
|
||||
onSettingsModified( { ...settings, prefers_community_taxa: !settings.prefers_community_taxa } );
|
||||
}}>
|
||||
<Text style={[textStyles.title, textStyles.marginTop]}>
|
||||
{t( "Community-Moderation-Settings" )}
|
||||
</Text>
|
||||
<Pressable
|
||||
style={[viewStyles.row, viewStyles.notificationCheckbox]}
|
||||
onPress={() => {
|
||||
onSettingsModified( {
|
||||
...settings,
|
||||
prefers_community_taxa: !settings.prefers_community_taxa
|
||||
} );
|
||||
}}
|
||||
>
|
||||
<CheckBox
|
||||
value={settings.prefers_community_taxa}
|
||||
onValueChange={( v ) => {
|
||||
onValueChange={v => {
|
||||
onSettingsModified( { ...settings, prefers_community_taxa: v } );
|
||||
}}
|
||||
tintColors={{false: colors.inatGreen, true: colors.inatGreen}}
|
||||
tintColors={{ false: colors.inatGreen, true: colors.inatGreen }}
|
||||
/>
|
||||
<Text style={textStyles.notificationTitle}>{t( "Accept-community-identifications" )}</Text>
|
||||
</Pressable>
|
||||
<Text style={textStyles.subTitle}>{t( "Who-can-add-observation-fields-to-my-observations" )}</Text>
|
||||
<Text style={textStyles.subTitle}>
|
||||
{t( "Who-can-add-observation-fields-to-my-observations" )}
|
||||
</Text>
|
||||
<View style={viewStyles.selectorContainer}>
|
||||
<Picker
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={settings.preferred_observation_fields_by}
|
||||
onValueChange={( itemValue, itemIndex ) =>
|
||||
onSettingsModified( { ...settings, preferred_observation_fields_by: itemValue } )
|
||||
}>
|
||||
{Object.keys( ADD_OBSERVATION_FIELDS ).map( ( k ) => (
|
||||
onValueChange={( itemValue, _itemIndex ) => onSettingsModified( {
|
||||
...settings,
|
||||
preferred_observation_fields_by: itemValue
|
||||
} )}
|
||||
>
|
||||
{Object.keys( ADD_OBSERVATION_FIELDS ).map( k => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={ADD_OBSERVATION_FIELDS[k]}
|
||||
value={k} />
|
||||
value={k}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
@@ -184,26 +223,32 @@ const SettingsContentDisplay = ( { settings, onSettingsModified }: SettingsProps
|
||||
<LicenseSelector
|
||||
title="Default observation license"
|
||||
value={settings.preferred_observation_license}
|
||||
onValueChanged={( v ) => onSettingsModified( { ...settings, preferred_observation_license: v} )}
|
||||
onValueChanged={v => onSettingsModified( {
|
||||
...settings,
|
||||
preferred_observation_license: v
|
||||
} )}
|
||||
updateExistingTitle="Update existing observations with new license choices"
|
||||
updateExisting={settings.make_observation_licenses_same}
|
||||
onUpdateExisting={( v ) => onSettingsModified( { ...settings, make_observation_licenses_same: v} )}
|
||||
onUpdateExisting={v => onSettingsModified( {
|
||||
...settings,
|
||||
make_observation_licenses_same: v
|
||||
} )}
|
||||
/>
|
||||
<LicenseSelector
|
||||
title="Default photo license"
|
||||
value={settings.preferred_photo_license}
|
||||
onValueChanged={( v ) => onSettingsModified( { ...settings, preferred_photo_license: v} )}
|
||||
onValueChanged={v => onSettingsModified( { ...settings, preferred_photo_license: v } )}
|
||||
updateExistingTitle="Update existing photos with new license choices"
|
||||
updateExisting={settings.make_photo_licenses_same}
|
||||
onUpdateExisting={( v ) => onSettingsModified( { ...settings, make_photo_licenses_same: v} )}
|
||||
onUpdateExisting={v => onSettingsModified( { ...settings, make_photo_licenses_same: v } )}
|
||||
/>
|
||||
<LicenseSelector
|
||||
title="Default sound license"
|
||||
value={settings.preferred_sound_license}
|
||||
onValueChanged={( v ) => onSettingsModified( { ...settings, preferred_sound_license: v} )}
|
||||
onValueChanged={v => onSettingsModified( { ...settings, preferred_sound_license: v } )}
|
||||
updateExistingTitle="Update existing sounds with new license choices"
|
||||
updateExisting={settings.make_sound_licenses_same}
|
||||
onUpdateExisting={( v ) => onSettingsModified( { ...settings, make_sound_licenses_same: v} )}
|
||||
onUpdateExisting={v => onSettingsModified( { ...settings, make_sound_licenses_same: v } )}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,43 +1,48 @@
|
||||
// @flow
|
||||
|
||||
import {Pressable, Text, View} from "react-native";
|
||||
import {viewStyles, textStyles} from "../../styles/settings/settings";
|
||||
import React from "react";
|
||||
import type { Node } from "react";
|
||||
import Switch from "react-native/Libraries/Components/Switch/Switch";
|
||||
import CheckBox from "@react-native-community/checkbox";
|
||||
import { colors } from "../../styles/global";
|
||||
import type { SettingsProps } from "./types";
|
||||
import { t } from "i18next";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import Switch from "react-native/Libraries/Components/Switch/Switch";
|
||||
|
||||
import colors from "../../styles/colors";
|
||||
import { textStyles, viewStyles } from "../../styles/settings/settings";
|
||||
import type { SettingsProps } from "./types";
|
||||
|
||||
const EMAIL_NOTIFICATIONS = {
|
||||
"Comments": "prefers_comment_email_notification",
|
||||
"Identifications": "prefers_identification_email_notification",
|
||||
"Mentions": "prefers_mention_email_notification",
|
||||
"Messages": "prefers_message_email_notification",
|
||||
Comments: "prefers_comment_email_notification",
|
||||
Identifications: "prefers_identification_email_notification",
|
||||
Mentions: "prefers_mention_email_notification",
|
||||
Messages: "prefers_message_email_notification",
|
||||
"Project journal posts": "prefers_project_journal_post_email_notification",
|
||||
// eslint-disable-next-line max-len
|
||||
"When a project adds your observations": "prefers_project_added_your_observation_email_notification",
|
||||
"Project curator changes": "prefers_project_curator_change_email_notification",
|
||||
"Taxonomy changes": "prefers_taxon_change_email_notification",
|
||||
"Observations by people I follow": "prefers_user_observation_email_notification",
|
||||
// eslint-disable-next-line max-len
|
||||
"Observations of taxa or from places that I subscribe to": "prefers_taxon_or_place_observation_email_notification"
|
||||
};
|
||||
|
||||
|
||||
const EmailNotification = ( { title, value, onValueChange } ): Node => (
|
||||
<Pressable style={[viewStyles.row, viewStyles.notificationCheckbox]} onPress={() => onValueChange( !value )}>
|
||||
<Pressable
|
||||
style={[viewStyles.row, viewStyles.notificationCheckbox]}
|
||||
onPress={() => onValueChange( !value )}
|
||||
>
|
||||
<CheckBox
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
tintColors={{false: colors.inatGreen, true: colors.inatGreen}}
|
||||
tintColors={{ false: colors.inatGreen, true: colors.inatGreen }}
|
||||
/>
|
||||
<Text style={textStyles.notificationTitle}>{title}</Text>
|
||||
<Text style={textStyles.notificationTitle}>{title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
|
||||
const Notification = ( { title, description, value, onValueChange } ): Node => (
|
||||
const Notification = ( {
|
||||
title, description, value, onValueChange
|
||||
} ): Node => (
|
||||
<View style={[viewStyles.row, viewStyles.notificationContainer]}>
|
||||
<View style={[viewStyles.column, viewStyles.notificationLeftSide]}>
|
||||
<Text style={textStyles.notificationTitle}>{title}</Text>
|
||||
@@ -50,41 +55,49 @@ const Notification = ( { title, description, value, onValueChange } ): Node => (
|
||||
);
|
||||
|
||||
const SettingsNotifications = ( { settings, onSettingsModified }: SettingsProps ): Node => (
|
||||
<>
|
||||
<Text style={textStyles.title}>{t( "iNaturalist-Activity-Notifications" )}</Text>
|
||||
<Notification
|
||||
title="Notify me of mentions (e.g. @username)"
|
||||
description="If you turn this off, you will not get any notifications when someone mentions you on iNaturalist."
|
||||
value={settings.prefers_receive_mentions}
|
||||
onValueChange={( v ) => onSettingsModified( { ...settings, prefers_receive_mentions: v} )}
|
||||
/>
|
||||
<Notification
|
||||
title="Confirming ID's"
|
||||
description="If you turn this off, you will no longer be notified about IDs that agree with yours."
|
||||
value={settings.prefers_redundant_identification_notifications}
|
||||
onValueChange={( v ) => onSettingsModified( { ...settings, prefers_redundant_identification_notifications: v} )}
|
||||
/>
|
||||
<Text style={textStyles.title}>{t( "Email-Notifications" )}</Text>
|
||||
<Notification
|
||||
title="Receive Email Notifications"
|
||||
description="If you turn this off, you will no longer receive any emails from iNaturalist regarding notifications."
|
||||
value={!settings.prefers_no_email}
|
||||
onValueChange={( v ) => onSettingsModified( { ...settings, prefers_no_email: !v} )}
|
||||
/>
|
||||
<>
|
||||
<Text style={textStyles.title}>{t( "iNaturalist-Activity-Notifications" )}</Text>
|
||||
<Notification
|
||||
title="Notify me of mentions (e.g. @username)"
|
||||
// eslint-disable-next-line max-len
|
||||
description="If you turn this off, you will not get any notifications when someone mentions you on iNaturalist."
|
||||
value={settings.prefers_receive_mentions}
|
||||
onValueChange={v => onSettingsModified( { ...settings, prefers_receive_mentions: v } )}
|
||||
/>
|
||||
<Notification
|
||||
title="Confirming ID's"
|
||||
// eslint-disable-next-line max-len
|
||||
description="If you turn this off, you will no longer be notified about IDs that agree with yours."
|
||||
value={settings.prefers_redundant_identification_notifications}
|
||||
onValueChange={v => onSettingsModified( {
|
||||
...settings,
|
||||
prefers_redundant_identification_notifications: v
|
||||
} )}
|
||||
/>
|
||||
<Text style={textStyles.title}>{t( "Email-Notifications" )}</Text>
|
||||
<Notification
|
||||
title="Receive Email Notifications"
|
||||
// eslint-disable-next-line max-len
|
||||
description="If you turn this off, you will no longer receive any emails from iNaturalist regarding notifications."
|
||||
value={!settings.prefers_no_email}
|
||||
onValueChange={v => onSettingsModified( { ...settings, prefers_no_email: !v } )}
|
||||
/>
|
||||
|
||||
{!settings.prefers_no_email &&
|
||||
{!settings.prefers_no_email
|
||||
&& (
|
||||
<>
|
||||
{Object.keys( EMAIL_NOTIFICATIONS ).map( ( k ) => (
|
||||
{Object.keys( EMAIL_NOTIFICATIONS ).map( k => (
|
||||
<EmailNotification
|
||||
key={k}
|
||||
title={k}
|
||||
value={settings[EMAIL_NOTIFICATIONS[k]]}
|
||||
// $FlowIgnore
|
||||
onValueChange={( v ) => onSettingsModified( { ...settings, [EMAIL_NOTIFICATIONS[k]]: v } )}
|
||||
onValueChange={v => onSettingsModified( { ...settings, [EMAIL_NOTIFICATIONS[k]]: v } )}
|
||||
/>
|
||||
) )}
|
||||
</>}
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
export { SettingsNotifications, EMAIL_NOTIFICATIONS };
|
||||
export { EMAIL_NOTIFICATIONS, SettingsNotifications };
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
// @flow
|
||||
|
||||
import {Button, Image, Text, TextInput, View} from "react-native";
|
||||
import {viewStyles} from "../../styles/settings/settings";
|
||||
import { t } from "i18next";
|
||||
// $FlowIgnore
|
||||
import {launchImageLibrary} from "react-native-image-picker";
|
||||
import React from "react";
|
||||
import type { Node } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
Button, Image, Text, TextInput, View
|
||||
} from "react-native";
|
||||
// $FlowIgnore
|
||||
import { launchImageLibrary } from "react-native-image-picker";
|
||||
|
||||
import { viewStyles } from "../../styles/settings/settings";
|
||||
import type { SettingsProps } from "./types";
|
||||
|
||||
const SettingsProfile = ( { settings, onSettingsModified }: SettingsProps ): Node => {
|
||||
|
||||
let profileSource;
|
||||
if ( settings.newProfilePhoto && !settings.removeProfilePhoto ) {
|
||||
profileSource = { uri: settings.newProfilePhoto.uri };
|
||||
} else if (
|
||||
settings.icon && !settings.removeProfilePhoto ) { profileSource = { uri: settings.icon };
|
||||
settings.icon && !settings.removeProfilePhoto ) {
|
||||
profileSource = { uri: settings.icon };
|
||||
} else {
|
||||
profileSource = require( "./../../images/profile.png" );
|
||||
profileSource = require( "../../images/profile.png" );
|
||||
}
|
||||
|
||||
const onImageSelected = ( response ) => {
|
||||
if ( response.didCancel ) {return;}
|
||||
onSettingsModified( { ...settings, newProfilePhoto: response.assets[0], removeProfilePhoto: false } );
|
||||
const onImageSelected = response => {
|
||||
if ( response.didCancel ) { return; }
|
||||
onSettingsModified( {
|
||||
...settings,
|
||||
newProfilePhoto: response.assets[0],
|
||||
removeProfilePhoto: false
|
||||
} );
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -34,15 +41,21 @@ const SettingsProfile = ( { settings, onSettingsModified }: SettingsProps ): Nod
|
||||
source={profileSource}
|
||||
/>
|
||||
<View style={viewStyles.column}>
|
||||
<Button title="Upload New Photo" onPress={() => launchImageLibrary( {}, onImageSelected )} />
|
||||
<Button title="Remove Photo" onPress={() => onSettingsModified( { ...settings, removeProfilePhoto: true } )} />
|
||||
<Button
|
||||
title="Upload New Photo"
|
||||
onPress={() => launchImageLibrary( {}, onImageSelected )}
|
||||
/>
|
||||
<Button
|
||||
title="Remove Photo"
|
||||
onPress={() => onSettingsModified( { ...settings, removeProfilePhoto: true } )}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={viewStyles.column}>
|
||||
<Text>{t( "Username" )}</Text>
|
||||
<TextInput
|
||||
style={viewStyles.textInput}
|
||||
onChangeText={( x ) => onSettingsModified( { ...settings, login: x} )}
|
||||
onChangeText={x => onSettingsModified( { ...settings, login: x } )}
|
||||
value={settings.login}
|
||||
/>
|
||||
</View>
|
||||
@@ -50,7 +63,7 @@ const SettingsProfile = ( { settings, onSettingsModified }: SettingsProps ): Nod
|
||||
<Text>{t( "Email" )}</Text>
|
||||
<TextInput
|
||||
style={viewStyles.textInput}
|
||||
onChangeText={( x ) => onSettingsModified( { ...settings, email: x} )}
|
||||
onChangeText={x => onSettingsModified( { ...settings, email: x } )}
|
||||
value={settings.email}
|
||||
/>
|
||||
</View>
|
||||
@@ -58,7 +71,7 @@ const SettingsProfile = ( { settings, onSettingsModified }: SettingsProps ): Nod
|
||||
<Text>{t( "Display-Name" )}</Text>
|
||||
<TextInput
|
||||
style={viewStyles.textInput}
|
||||
onChangeText={( x ) => onSettingsModified( { ...settings, name: x} )}
|
||||
onChangeText={x => onSettingsModified( { ...settings, name: x } )}
|
||||
value={settings.name}
|
||||
/>
|
||||
</View>
|
||||
@@ -68,7 +81,7 @@ const SettingsProfile = ( { settings, onSettingsModified }: SettingsProps ): Nod
|
||||
style={viewStyles.textInput}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
onChangeText={( x ) => onSettingsModified( { ...settings, description: x} )}
|
||||
onChangeText={x => onSettingsModified( { ...settings, description: x } )}
|
||||
value={settings.description}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import { Alert, Image, Text, TextInput, View } from "react-native";
|
||||
import {viewStyles, textStyles} from "../../styles/settings/settings";
|
||||
import React, { useEffect, useMemo, useCallback } from "react";
|
||||
import type { Node } from "react";
|
||||
import { Picker } from "@react-native-picker/picker";
|
||||
import { t } from "i18next";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import {useDebounce} from "use-debounce";
|
||||
import inatjs from "inaturalistjs";
|
||||
import {Picker} from "@react-native-picker/picker";
|
||||
import {colors} from "../../styles/global";
|
||||
import CheckBox from "@react-native-community/checkbox";
|
||||
import UserSearchInput from "./UserSearchInput";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import {
|
||||
Alert, Image, Text, TextInput, View
|
||||
} from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
import colors from "../../styles/colors";
|
||||
import { textStyles, viewStyles } from "../../styles/settings/settings";
|
||||
import BlockedUser from "./BlockedUser";
|
||||
import useRelationships from "./hooks/useRelationships";
|
||||
import MutedUser from "./MutedUser";
|
||||
import Relationship from "./Relationship";
|
||||
import UserSearchInput from "./UserSearchInput";
|
||||
|
||||
const FOLLOWING = {
|
||||
any: "All",
|
||||
@@ -66,15 +71,32 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
orderBy = "user";
|
||||
order = "desc";
|
||||
}
|
||||
const relationshipParams = {q: finalUserSearch, following, trusted, order_by: orderBy, order: order, per_page: 10, page, random: refreshRelationships };
|
||||
const [relationshipResults, perPage, totalResults] = useRelationships( accessToken, relationshipParams );
|
||||
const relationshipParams = {
|
||||
q: finalUserSearch,
|
||||
following,
|
||||
trusted,
|
||||
order_by: orderBy,
|
||||
order,
|
||||
per_page: 10,
|
||||
page,
|
||||
random: refreshRelationships
|
||||
};
|
||||
const [
|
||||
relationshipResults,
|
||||
perPage,
|
||||
totalResults
|
||||
] = useRelationships( accessToken, relationshipParams );
|
||||
const totalPages = totalResults > 0 && perPage > 0 ? Math.ceil( totalResults / perPage ) : 1;
|
||||
|
||||
useEffect( () => {
|
||||
const getBlockedUsers = async () => {
|
||||
try {
|
||||
const responses = await Promise.all( settings.blocked_user_ids.map( ( userId ) => inatjs.users.fetch( userId, { fields: "icon,login,name"} ) ) );
|
||||
setBlockedUsers( responses.map( ( r ) => r.results[0] ) );
|
||||
const responses = await Promise.all(
|
||||
settings.blocked_user_ids.map(
|
||||
userId => inatjs.users.fetch( userId, { fields: "icon,login,name" } )
|
||||
)
|
||||
);
|
||||
setBlockedUsers( responses.map( r => r.results[0] ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
@@ -85,7 +107,6 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if ( settings.blocked_user_ids.length > 0 ) {
|
||||
@@ -96,8 +117,12 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
|
||||
const getMutedUsers = async () => {
|
||||
try {
|
||||
const responses = await Promise.all( settings.muted_user_ids.map( ( userId ) => inatjs.users.fetch( userId, { fields: "icon,login,name" } ) ) );
|
||||
setMutedUsers( responses.map( ( r ) => r.results[0] ) );
|
||||
const responses = await Promise.all(
|
||||
settings.muted_user_ids.map(
|
||||
userId => inatjs.users.fetch( userId, { fields: "icon,login,name" } )
|
||||
)
|
||||
);
|
||||
setMutedUsers( responses.map( r => r.results[0] ) );
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
@@ -108,7 +133,6 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
cancelable: true
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if ( settings.muted_user_ids.length > 0 ) {
|
||||
@@ -118,13 +142,12 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
}
|
||||
}, [settings] );
|
||||
|
||||
|
||||
const updateRelationship = useCallback( async ( relationship, update ) => {
|
||||
let response;
|
||||
try {
|
||||
response = await inatjs.relationships.update(
|
||||
{ id: relationship.id, relationship: update },
|
||||
{ api_token: accessToken}
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
@@ -142,17 +165,19 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
setRefreshRelationships( Math.random() );
|
||||
}, [accessToken] );
|
||||
|
||||
const askToRemoveRelationship = useCallback( ( relationship ) => {
|
||||
const askToRemoveRelationship = useCallback( relationship => {
|
||||
Alert.alert(
|
||||
"Remove Relationship?",
|
||||
`You will no longer be following or trusting ${relationship.friendUser.login}.`,
|
||||
[
|
||||
{ text: "Remove Relationship", onPress: async () => {
|
||||
{
|
||||
text: "Remove Relationship",
|
||||
onPress: async () => {
|
||||
let response;
|
||||
try {
|
||||
response = await inatjs.relationships.delete(
|
||||
{ id: relationship.id },
|
||||
{ api_token: accessToken}
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
@@ -168,7 +193,8 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
}
|
||||
console.log( response );
|
||||
setRefreshRelationships( Math.random() );
|
||||
} }
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
cancelable: true
|
||||
@@ -176,13 +202,12 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
);
|
||||
}, [accessToken] );
|
||||
|
||||
|
||||
const unblockUser = useCallback( async ( user ) => {
|
||||
const unblockUser = useCallback( async user => {
|
||||
let response;
|
||||
try {
|
||||
response = await inatjs.users.unblock(
|
||||
{ id: user.id },
|
||||
{ api_token: accessToken}
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
@@ -200,15 +225,15 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
onRefreshUser();
|
||||
}, [accessToken, onRefreshUser] );
|
||||
|
||||
const blockUser = async ( user ) => {
|
||||
if ( !user ) {return;}
|
||||
const blockUser = async user => {
|
||||
if ( !user ) { return; }
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await inatjs.users.block(
|
||||
{ id: user.id },
|
||||
{ api_token: accessToken}
|
||||
);
|
||||
response = await inatjs.users.block(
|
||||
{ id: user.id },
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
Alert.alert(
|
||||
@@ -225,27 +250,12 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
onRefreshUser();
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
const BlockedUser = useMemo( ( { user } ): Node => {
|
||||
return <View style={[viewStyles.row, viewStyles.relationshipRow]}>
|
||||
<Image
|
||||
style={viewStyles.relationshipImage}
|
||||
source={{ uri: user.icon}}
|
||||
/>
|
||||
<View style={viewStyles.column}>
|
||||
<Text>{user.login}</Text>
|
||||
<Text>{user.name}</Text>
|
||||
</View>
|
||||
<Pressable style={viewStyles.removeRelationship} onPress={() => unblockUser( user )}><Text>{t( "Unblock" )}</Text></Pressable>
|
||||
</View>;
|
||||
}, [unblockUser] );
|
||||
|
||||
const unmuteUser = useCallback( async ( user ) => {
|
||||
const unmuteUser = useCallback( async user => {
|
||||
let response;
|
||||
try {
|
||||
response = await inatjs.users.unmute(
|
||||
{ id: user.id },
|
||||
{ api_token: accessToken}
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
@@ -263,15 +273,14 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
onRefreshUser();
|
||||
}, [accessToken, onRefreshUser] );
|
||||
|
||||
|
||||
const muteUser = async ( user ) => {
|
||||
if ( !user ) {return;}
|
||||
const muteUser = async user => {
|
||||
if ( !user ) { return; }
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await inatjs.users.mute(
|
||||
{ id: user.id },
|
||||
{ api_token: accessToken}
|
||||
{ api_token: accessToken }
|
||||
);
|
||||
} catch ( e ) {
|
||||
console.error( e );
|
||||
@@ -289,58 +298,6 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
onRefreshUser();
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
const MutedUser = useMemo( ( {user} ): Node => {
|
||||
return <View style={[viewStyles.row, viewStyles.relationshipRow]}>
|
||||
<Image
|
||||
style={viewStyles.relationshipImage}
|
||||
source={{ uri: user.icon}}
|
||||
/>
|
||||
<View style={viewStyles.column}>
|
||||
<Text>{user.login}</Text>
|
||||
<Text>{user.name}</Text>
|
||||
</View>
|
||||
<Pressable style={viewStyles.removeRelationship} onPress={() => unmuteUser( user )}><Text>{t( "Unmute" )}</Text></Pressable>
|
||||
</View>;
|
||||
}, [unmuteUser] );
|
||||
|
||||
// $FlowFixMe
|
||||
const Relationship = useMemo( ( {relationship} ): Node => {
|
||||
return <View style={[viewStyles.column, viewStyles.relationshipRow]}>
|
||||
<View style={viewStyles.row}>
|
||||
<Image
|
||||
style={viewStyles.relationshipImage}
|
||||
source={{ uri: relationship.friendUser.icon_url}}
|
||||
/>
|
||||
<View style={viewStyles.column}>
|
||||
<Text>{relationship.friendUser.login}</Text>
|
||||
<Text>{relationship.friendUser.name}</Text>
|
||||
</View>
|
||||
<View style={viewStyles.column}>
|
||||
<View style={[viewStyles.row, viewStyles.notificationCheckbox]}>
|
||||
<CheckBox
|
||||
value={relationship.following}
|
||||
onValueChange={( x ) => { updateRelationship( relationship, { following: !relationship.following } ); }}
|
||||
tintColors={{false: colors.inatGreen, true: colors.inatGreen}}
|
||||
/>
|
||||
<Text>{t( "Following" )}</Text>
|
||||
</View>
|
||||
<View style={[viewStyles.row, viewStyles.notificationCheckbox]}>
|
||||
<CheckBox
|
||||
value={relationship.trust}
|
||||
onValueChange={( x ) => { updateRelationship( relationship, { trust: !relationship.trust } ); }}
|
||||
tintColors={{false: colors.inatGreen, true: colors.inatGreen}}
|
||||
/>
|
||||
<Text>{t( "Trust-with-hidden-coordinates" )}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text>{t( "Added-on-date", { date: relationship.created_at } )}</Text>
|
||||
<Pressable style={viewStyles.removeRelationship} onPress={() => askToRemoveRelationship( relationship )}><Text>{t( "Remove-Relationship" )}</Text></Pressable>
|
||||
</View>;
|
||||
}, [askToRemoveRelationship, updateRelationship] );
|
||||
|
||||
|
||||
return (
|
||||
// $FlowFixMe
|
||||
<View style={viewStyles.column}>
|
||||
@@ -348,14 +305,17 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
<View style={viewStyles.row}>
|
||||
<TextInput
|
||||
style={viewStyles.textInput}
|
||||
onChangeText={( v ) => {
|
||||
onChangeText={v => {
|
||||
setUserSearch( v );
|
||||
}}
|
||||
value={userSearch}
|
||||
/>
|
||||
<Pressable style={viewStyles.clearSearch} onPress={() => {
|
||||
setUserSearch( "" );
|
||||
}}>
|
||||
<Pressable
|
||||
style={viewStyles.clearSearch}
|
||||
onPress={() => {
|
||||
setUserSearch( "" );
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={viewStyles.clearSearch}
|
||||
resizeMode="contain"
|
||||
@@ -370,14 +330,14 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={following}
|
||||
onValueChange={( itemValue, itemIndex ) =>
|
||||
setFollowing( itemValue )
|
||||
}>
|
||||
{Object.keys( FOLLOWING ).map( ( k ) => (
|
||||
onValueChange={( itemValue, _itemIndex ) => setFollowing( itemValue )}
|
||||
>
|
||||
{Object.keys( FOLLOWING ).map( k => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={FOLLOWING[k]}
|
||||
value={k} />
|
||||
value={k}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
@@ -390,14 +350,14 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={trusted}
|
||||
onValueChange={( itemValue, itemIndex ) =>
|
||||
setTrusted( itemValue )
|
||||
}>
|
||||
{Object.keys( TRUSTED ).map( ( k ) => (
|
||||
onValueChange={( itemValue, _itemIndex ) => setTrusted( itemValue )}
|
||||
>
|
||||
{Object.keys( TRUSTED ).map( k => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={TRUSTED[k]}
|
||||
value={k} />
|
||||
value={k}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
@@ -410,43 +370,65 @@ const SettingsRelationships = ( { accessToken, settings, onRefreshUser }: Props
|
||||
style={viewStyles.selector}
|
||||
dropdownIconColor={colors.inatGreen}
|
||||
selectedValue={sortBy}
|
||||
onValueChange={( itemValue, itemIndex ) =>
|
||||
setSortBy( itemValue )
|
||||
}>
|
||||
{Object.keys( SORT_BY ).map( ( k ) => (
|
||||
onValueChange={( itemValue, _itemIndex ) => setSortBy( itemValue )}
|
||||
>
|
||||
{Object.keys( SORT_BY ).map( k => (
|
||||
<Picker.Item
|
||||
key={k}
|
||||
label={SORT_BY[k]}
|
||||
value={k} />
|
||||
value={k}
|
||||
/>
|
||||
) )}
|
||||
</Picker>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{relationshipResults.map( ( relationship ) => (
|
||||
// $FlowFixMe
|
||||
<Relationship key={relationship.id} relationship={relationship} />
|
||||
{relationshipResults.map( relationship => (
|
||||
<Relationship
|
||||
key={relationship.id}
|
||||
relationship={relationship}
|
||||
updateRelationship={updateRelationship}
|
||||
askToRemoveRelationship={askToRemoveRelationship}
|
||||
/>
|
||||
) )}
|
||||
{ totalPages > 1 && <View style={[viewStyles.row, viewStyles.paginationContainer]}>
|
||||
<Pressable disabled={page === 1} style={viewStyles.pageButton} onPress={() => setPage( page - 1 )}><Text><</Text></Pressable>
|
||||
{[...Array( totalPages ).keys()].map( ( x ) => (
|
||||
<Pressable key={x} style={viewStyles.pageButton} onPress={() => setPage( x + 1 )}><Text style={x + 1 === page ? textStyles.currentPage : null}>{x + 1}</Text></Pressable>
|
||||
{ totalPages > 1 && (
|
||||
<View style={[viewStyles.row, viewStyles.paginationContainer]}>
|
||||
<Pressable
|
||||
disabled={page === 1}
|
||||
style={viewStyles.pageButton}
|
||||
onPress={() => setPage( page - 1 )}
|
||||
>
|
||||
<Text><</Text>
|
||||
</Pressable>
|
||||
{[...Array( totalPages ).keys()].map( x => (
|
||||
<Pressable
|
||||
key={x}
|
||||
style={viewStyles.pageButton}
|
||||
onPress={() => setPage( x + 1 )}
|
||||
>
|
||||
<Text style={x + 1 === page ? textStyles.currentPage : null}>{x + 1}</Text>
|
||||
</Pressable>
|
||||
) )}
|
||||
<Pressable disabled={page === totalPages} style={viewStyles.pageButton} onPress={() => setPage( page + 1 )}><Text>></Text></Pressable>
|
||||
</View>}
|
||||
<Pressable
|
||||
disabled={page === totalPages}
|
||||
style={viewStyles.pageButton}
|
||||
onPress={() => setPage( page + 1 )}
|
||||
>
|
||||
<Text>></Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text style={textStyles.title}>{t( "Blocked-Users" )}</Text>
|
||||
<UserSearchInput userId={0} onUserChanged={( u ) => blockUser( u )} />
|
||||
{blockedUsers.map( ( user ) => (
|
||||
// $FlowFixMe
|
||||
<BlockedUser key={user.id} user={user} />
|
||||
<UserSearchInput userId={0} onUserChanged={u => blockUser( u )} />
|
||||
{blockedUsers.map( user => (
|
||||
<BlockedUser key={user.id} user={user} unblockUser={unblockUser} />
|
||||
) )}
|
||||
|
||||
<Text style={textStyles.title}>{t( "Muted-Users" )}</Text>
|
||||
<UserSearchInput userId={0} onUserChanged={( u ) => muteUser( u )} />
|
||||
{mutedUsers.map( ( user ) => (
|
||||
// $FlowFixMe
|
||||
<MutedUser key={user.id} user={user} />
|
||||
<UserSearchInput userId={0} onUserChanged={u => muteUser( u )} />
|
||||
{mutedUsers.map( user => (
|
||||
<MutedUser key={user.id} user={user} unmuteUser={unmuteUser} />
|
||||
) )}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import React, {useEffect} from "react";
|
||||
import {useDebounce} from "use-debounce";
|
||||
import {Image, Text, TextInput, View} from "react-native";
|
||||
import {textStyles, viewStyles} from "../../styles/settings/settings";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Image, Text, TextInput, View
|
||||
} from "react-native";
|
||||
import Pressable from "react-native/Libraries/Components/Pressable/Pressable";
|
||||
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
const UserSearchInput = ( { onUserChanged} ): React.Node => {
|
||||
import useRemoteSearchResults from "../../sharedHooks/useRemoteSearchResults";
|
||||
import { textStyles, viewStyles } from "../../styles/settings/settings";
|
||||
|
||||
const UserSearchInput = ( { onUserChanged } ): React.Node => {
|
||||
const [hideResults, setHideResults] = React.useState( true );
|
||||
const [userSearch, setUserSearch] = React.useState( "" );
|
||||
// So we'll start searching only once the user finished typing
|
||||
const [finalUserSearch] = useDebounce( userSearch, 500 );
|
||||
const userResults = useRemoteSearchResults( finalUserSearch, "users", "user.login,user.name,user.icon" ).map( r => r.user );
|
||||
const userResults = useRemoteSearchResults(
|
||||
finalUserSearch,
|
||||
"users",
|
||||
"user.login,user.name,user.icon"
|
||||
).map( r => r.user );
|
||||
|
||||
useEffect( () => {
|
||||
if ( finalUserSearch.length === 0 ) {
|
||||
@@ -18,22 +25,25 @@ const UserSearchInput = ( { onUserChanged} ): React.Node => {
|
||||
}
|
||||
}, [finalUserSearch] );
|
||||
|
||||
return (
|
||||
return (
|
||||
<View style={viewStyles.column}>
|
||||
<View style={viewStyles.row}>
|
||||
<TextInput
|
||||
style={viewStyles.textInput}
|
||||
onChangeText={( v ) => {
|
||||
onChangeText={v => {
|
||||
setHideResults( false );
|
||||
setUserSearch( v );
|
||||
}}
|
||||
value={userSearch}
|
||||
/>
|
||||
<Pressable style={viewStyles.clearSearch} onPress={() => {
|
||||
setHideResults( true );
|
||||
onUserChanged( null );
|
||||
setUserSearch( "" );
|
||||
}}>
|
||||
<Pressable
|
||||
style={viewStyles.clearSearch}
|
||||
onPress={() => {
|
||||
setHideResults( true );
|
||||
onUserChanged( null );
|
||||
setUserSearch( "" );
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={viewStyles.clearSearch}
|
||||
resizeMode="contain"
|
||||
@@ -41,13 +51,16 @@ const UserSearchInput = ( { onUserChanged} ): React.Node => {
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
{!hideResults && finalUserSearch.length > 0 && userResults.map( ( result ) => (
|
||||
<Pressable key={result.id} style={[viewStyles.row, viewStyles.placeResultContainer]}
|
||||
onPress={() => {
|
||||
setHideResults( true );
|
||||
onUserChanged( result );
|
||||
setUserSearch( result.login );
|
||||
}}>
|
||||
{!hideResults && finalUserSearch.length > 0 && userResults.map( result => (
|
||||
<Pressable
|
||||
key={result.id}
|
||||
style={[viewStyles.row, viewStyles.placeResultContainer]}
|
||||
onPress={() => {
|
||||
setHideResults( true );
|
||||
onUserChanged( result );
|
||||
setUserSearch( result.login );
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={viewStyles.userPic}
|
||||
resizeMode="contain"
|
||||
@@ -59,7 +72,6 @@ const UserSearchInput = ( { onUserChanged} ): React.Node => {
|
||||
) )}
|
||||
</View>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default UserSearchInput;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user