mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Pre-commit hooks; Realm updates
* Move realm object creation functions into realm classes * Make all fields except for uuids optional in realm & update schemaVersion * Add pre-commit hooks for linting * Add eslint automatic fix line * Testing pre-commit hook * Still testing * Tweak husky settings and remove lint-staged * Add a Taxon realm shared by Observation and Identification; add listener for obs list changes * Create new User realm model for Comments/Identifications * Remove code comments from models * Add loading wheel while ObsList is fetching initial data * Move observations into stack within drawer navigator * Remove unneeded code * Look up existing obs using .objectForPrimaryKey method * More code cleanup (naming, adding primary keys, etc.) * Merge testing code from main; remove unused imports * Add more factories and fix ObsCard tests * Update tests to match updated realm models * Move ObservationProvider to wrap MyObservationsStackNavigator instead of all navigation * Remove code comments * Add primary keys to User and Taxon schemas * Update realm models to include ObservationPhotos * Get integration test to pass with new realm schemas
This commit is contained in:
committed by
GitHub
parent
523da746f2
commit
e206970c8c
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
@@ -4,6 +4,9 @@
|
||||
1. Run `npm install`
|
||||
2. Run `npx pod-install ios` or `cd ios && pod install` from the root directory
|
||||
|
||||
## Set up pre-commit hooks
|
||||
1. We're using [Husky](https://typicode.github.io/husky/#/) to automatically run `eslint` before each commit. Run `npm run prepare` to install Husky locally.
|
||||
|
||||
## Run build
|
||||
1. Run `npm start -- --reset-cache` (`npm start` works too, but resetting the cache each time makes for a lot less build issues)
|
||||
2. Run `npm run ios` or `npm run android`
|
||||
|
||||
2
index.js
2
index.js
@@ -4,7 +4,7 @@ import "react-native-gesture-handler";
|
||||
|
||||
import {AppRegistry} from "react-native";
|
||||
import inatjs from "inaturalistjs";
|
||||
import App from "./src/navigation/stackNavigation";
|
||||
import App from "./src/navigation/rootNavigation";
|
||||
import {name as appName} from "./app.json";
|
||||
|
||||
inatjs.setConfig( {
|
||||
|
||||
58
package-lock.json
generated
58
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@react-navigation/drawer": "^6.1.8",
|
||||
"@react-navigation/elements": "^1.2.1",
|
||||
"@react-navigation/native": "^6.0.6",
|
||||
"@react-navigation/native-stack": "^6.2.4",
|
||||
"inaturalistjs": "github:inaturalist/inaturalistjs",
|
||||
@@ -40,6 +41,7 @@
|
||||
"factoria": "^3.2.2",
|
||||
"faker": "^5.5.3",
|
||||
"flow-bin": "^0.149.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^26.6.3",
|
||||
"metro-react-native-babel-preset": "^0.66.0",
|
||||
"react-native-accessibility-engine": "^0.6.0",
|
||||
@@ -3703,7 +3705,7 @@
|
||||
"react-native-screens": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/drawer/node_modules/@react-navigation/elements": {
|
||||
"node_modules/@react-navigation/elements": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.2.1.tgz",
|
||||
"integrity": "sha512-EnmAbKMsptrliRKf95rdgS6BhMjML+mIns06+G1Vdih6BrEo7/0iytThUv3WBf99AI76dyEq/cqLUwHPiFzXWg==",
|
||||
@@ -3744,17 +3746,6 @@
|
||||
"react-native-screens": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/native-stack/node_modules/@react-navigation/elements": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.2.1.tgz",
|
||||
"integrity": "sha512-EnmAbKMsptrliRKf95rdgS6BhMjML+mIns06+G1Vdih6BrEo7/0iytThUv3WBf99AI76dyEq/cqLUwHPiFzXWg==",
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^6.0.0",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-safe-area-context": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/native/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -8119,6 +8110,21 @@
|
||||
"node": ">=8.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz",
|
||||
"integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"husky": "lib/bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/hyphenate-style-name": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
|
||||
@@ -18396,16 +18402,14 @@
|
||||
"@react-navigation/elements": "^1.2.1",
|
||||
"color": "^3.1.3",
|
||||
"warn-once": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/elements": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.2.1.tgz",
|
||||
"integrity": "sha512-EnmAbKMsptrliRKf95rdgS6BhMjML+mIns06+G1Vdih6BrEo7/0iytThUv3WBf99AI76dyEq/cqLUwHPiFzXWg==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@react-navigation/elements": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.2.1.tgz",
|
||||
"integrity": "sha512-EnmAbKMsptrliRKf95rdgS6BhMjML+mIns06+G1Vdih6BrEo7/0iytThUv3WBf99AI76dyEq/cqLUwHPiFzXWg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-navigation/native": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.0.6.tgz",
|
||||
@@ -18430,14 +18434,6 @@
|
||||
"requires": {
|
||||
"@react-navigation/elements": "^1.2.1",
|
||||
"warn-once": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/elements": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.2.1.tgz",
|
||||
"integrity": "sha512-EnmAbKMsptrliRKf95rdgS6BhMjML+mIns06+G1Vdih6BrEo7/0iytThUv3WBf99AI76dyEq/cqLUwHPiFzXWg==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@react-navigation/routers": {
|
||||
@@ -21798,6 +21794,12 @@
|
||||
"integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
|
||||
"dev": true
|
||||
},
|
||||
"husky": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz",
|
||||
"integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"hyphenate-style-name": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"ios": "react-native run-ios --simulator='iPhone 13'",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/drawer": "^6.1.8",
|
||||
"@react-navigation/elements": "^1.2.1",
|
||||
"@react-navigation/native": "^6.0.6",
|
||||
"@react-navigation/native-stack": "^6.2.4",
|
||||
"inaturalistjs": "github:inaturalist/inaturalistjs",
|
||||
@@ -42,6 +44,7 @@
|
||||
"factoria": "^3.2.2",
|
||||
"faker": "^5.5.3",
|
||||
"flow-bin": "^0.149.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^26.6.3",
|
||||
"metro-react-native-babel-preset": "^0.66.0",
|
||||
"react-native-accessibility-engine": "^0.6.0",
|
||||
|
||||
@@ -12,14 +12,15 @@ type Props = {
|
||||
}
|
||||
|
||||
const ActivityTab = ( { ids }: Props ): React.Node => ids.map( id => {
|
||||
const taxon = id.taxon;
|
||||
// this should all perform similarly to the activity tab on web
|
||||
// https://github.com/inaturalist/inaturalist/blob/df6572008f60845b8ef5972a92a9afbde6f67829/app/webpack/observations/show/components/activity_item.jsx
|
||||
return (
|
||||
<View key={id.uuid}>
|
||||
<View style={viewStyles.userProfileRow}>
|
||||
<View style={viewStyles.userProfileRow}>
|
||||
<UserIcon uri={id.userIcon} />
|
||||
<Text>{`@${id.userLogin}`}</Text>
|
||||
<UserIcon uri={id.user.iconUrl} />
|
||||
<Text>{`@${id.user.login}`}</Text>
|
||||
</View>
|
||||
<Text>{id.body}</Text>
|
||||
{id.vision && <Text>vision</Text>}
|
||||
@@ -27,10 +28,10 @@ const ActivityTab = ( { ids }: Props ): React.Node => ids.map( id => {
|
||||
{id.createdAt && <Text>time (ago)</Text>}
|
||||
</View>
|
||||
<View style={viewStyles.speciesDetailRow}>
|
||||
<SmallSquareImage uri={id.taxonPhoto} />
|
||||
<SmallSquareImage uri={taxon.defaultPhotoSquareUrl} />
|
||||
<View>
|
||||
<Text style={textStyles.commonNameText}>{id.commonName}</Text>
|
||||
<Text style={textStyles.scientificNameText}>{id.rank} {id.name}</Text>
|
||||
<Text style={textStyles.commonNameText}>{taxon.preferredCommonName}</Text>
|
||||
<Text style={textStyles.scientificNameText}>{taxon.rank} {taxon.name}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ScrollView } from "react-native-gesture-handler";
|
||||
import { useNavigation } from "@react-navigation/core";
|
||||
|
||||
import { viewStyles, textStyles } from "../../styles/obsDetails";
|
||||
// import useFetchObsDetails from "./hooks/fetchObsDetails";
|
||||
import useFetchObsDetailsFromRealm from "./hooks/fetchObsFromRealm";
|
||||
import ActivityTab from "./ActivityTab";
|
||||
import UserIcon from "../SharedComponents/UserIcon";
|
||||
@@ -27,20 +26,24 @@ const ObsDetails = ( ): Node => {
|
||||
const navToUserProfile = ( ) => navigation.navigate( "UserProfile" );
|
||||
|
||||
const ids = observation && observation.identifications;
|
||||
const photos = observation && observation.photos;
|
||||
const photos = observation && observation.observationPhotos;
|
||||
|
||||
const showActivityTab = ( ) => setTab( 0 );
|
||||
const showDataTab = ( ) => setTab( 1 );
|
||||
|
||||
if ( !observation ) { return null; }
|
||||
|
||||
const taxon = observation.taxon;
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<ScrollView>
|
||||
<View style={viewStyles.userProfileRow}>
|
||||
<Pressable style={viewStyles.userProfileRow} onPress={navToUserProfile}>
|
||||
<UserIcon uri={observation.userProfilePhoto} />
|
||||
<Text>{`@${observation.userLogin}`}</Text>
|
||||
{/* TODO: fill user icon in with saved current user icon or icon from another user API call */}
|
||||
<UserIcon uri={null} />
|
||||
{/* TODO: fill in text with saved current user login or login from another user API call */}
|
||||
<Text>@username</Text>
|
||||
</Pressable>
|
||||
<Text>{observation.createdAt}</Text>
|
||||
</View>
|
||||
@@ -48,15 +51,15 @@ const ObsDetails = ( ): Node => {
|
||||
<PhotoScroll photos={photos} />
|
||||
</View>
|
||||
<View style={viewStyles.row}>
|
||||
<Image source={{ uri: observation.userPhoto }} style={viewStyles.imageBackground} />
|
||||
<Image source={{ uri: taxon.defaultPhotoSquareUrl }} style={viewStyles.imageBackground} />
|
||||
<View style={viewStyles.obsDetailsColumn}>
|
||||
<Text style={textStyles.text}>{observation.taxonRank}</Text>
|
||||
<Text style={textStyles.commonNameText}>{observation.commonName}</Text>
|
||||
<Text style={textStyles.scientificNameText}>scientific name</Text>
|
||||
<Text style={textStyles.text}>{taxon.rank}</Text>
|
||||
<Text style={textStyles.commonNameText}>{taxon.preferredCommonName}</Text>
|
||||
<Text style={textStyles.scientificNameText}>{taxon.name}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={textStyles.text}>{observation.identificationCount}</Text>
|
||||
<Text style={textStyles.text}>{observation.commentCount}</Text>
|
||||
<Text style={textStyles.text}>{observation.identifications.length}</Text>
|
||||
<Text style={textStyles.text}>{observation.comments.length}</Text>
|
||||
<Text style={textStyles.text}>{observation.qualityGrade}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -12,13 +12,14 @@ type Props = {
|
||||
|
||||
const PhotoScroll = ( { photos }: Props ): React.Node => {
|
||||
const renderImage = ( { item } ) => {
|
||||
let photoUrl = item.url.replace( "square", "large" );
|
||||
const photo = item.photo;
|
||||
let photoUrl = photo.url.replace( "square", "large" );
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image source={{ uri: photoUrl }} style={imageStyles.fullWidthImage} />
|
||||
<Pressable>
|
||||
<Text style={textStyles.license}>{item.licenseCode}</Text>
|
||||
<Text style={textStyles.license}>{photo.licenseCode}</Text>
|
||||
</Pressable>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import * as React from "react";
|
||||
import { Text, Pressable } from "react-native";
|
||||
|
||||
import { viewStyles } from "../../styles/observations/obsListRightHeader";
|
||||
import { viewStyles } from "../../styles/observations/messagesIcon";
|
||||
|
||||
const ObsListRightHeader = ( ): React.Node => (
|
||||
const MessagesIcon = ( ): React.Node => (
|
||||
<Pressable onPress={( ) => console.log( "navigate to messages" )} style={viewStyles.messages}>
|
||||
<Text>messages</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default ObsListRightHeader;
|
||||
export default MessagesIcon;
|
||||
@@ -20,18 +20,18 @@ const ObsCard = ( { item, handlePress }: Props ): Node => (
|
||||
accessibilityLabel="Navigate to observation details screen"
|
||||
>
|
||||
<Image
|
||||
source={{ uri: item.userPhoto }}
|
||||
source={{ uri: item.observationPhotos[0].photo.url }}
|
||||
style={viewStyles.imageBackground}
|
||||
testID="ObsList.photo"
|
||||
/>
|
||||
<View style={viewStyles.obsDetailsColumn}>
|
||||
<Text style={textStyles.text}>{item.commonName}</Text>
|
||||
<Text style={textStyles.text}>{item.taxon.preferredCommonName}</Text>
|
||||
<Text style={textStyles.text}>{item.placeGuess}</Text>
|
||||
<Text style={textStyles.text}>{item.timeObservedAt}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={textStyles.text}>{item.identificationCount}</Text>
|
||||
<Text style={textStyles.text} testID="ObsList.obsCard.commentCount">{item.commentCount}</Text>
|
||||
<Text style={textStyles.text}>{item.identifications.length}</Text>
|
||||
<Text style={textStyles.text} testID="ObsList.obsCard.commentCount">{item.comments.length}</Text>
|
||||
<Text style={textStyles.text}>{item.qualityGrade}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { FlatList, ActivityIndicator } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { Node } from "react";
|
||||
|
||||
@@ -23,7 +23,7 @@ const ObsList = ( ): Node => {
|
||||
// (and anytime you save while in debug - hot reloading mode )
|
||||
// this will eventually go in a sync button / pull-from-top gesture
|
||||
// instead of automatically fetching every time the component loads
|
||||
useFetchObservations( );
|
||||
const loading = useFetchObservations( );
|
||||
|
||||
const extractKey = item => item.uuid;
|
||||
const renderItem = ( { item } ) => <ObsCard item={item} handlePress={navToObsDetails} />;
|
||||
@@ -43,13 +43,18 @@ const ObsList = ( ): Node => {
|
||||
|
||||
return (
|
||||
<ViewWithFooter>
|
||||
<FlatList
|
||||
data={observationList}
|
||||
keyExtractor={extractKey}
|
||||
renderItem={renderItem}
|
||||
testID="ObsList.myObservations"
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
/>
|
||||
{loading
|
||||
? <ActivityIndicator />
|
||||
: (
|
||||
<FlatList
|
||||
data={observationList}
|
||||
keyExtractor={extractKey}
|
||||
renderItem={renderItem}
|
||||
testID="ObsList.myObservations"
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ViewWithFooter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,41 +1,102 @@
|
||||
// @flow
|
||||
|
||||
import { useEffect, useMemo, useCallback, useRef, useState } from "react";
|
||||
import { useEffect, useCallback, useRef, useState } from "react";
|
||||
import inatjs from "inaturalistjs";
|
||||
import Realm from "realm";
|
||||
|
||||
import realmConfig from "../../../models/index";
|
||||
import Observation from "../../../models/Observation";
|
||||
import Taxon from "../../../models/Taxon";
|
||||
|
||||
const useFetchObservations = ( ): Array<Object> => {
|
||||
const [observations, setObservations] = useState( [] );
|
||||
const USER_FIELDS = {
|
||||
icon_url: true,
|
||||
id: true,
|
||||
login: true,
|
||||
name: true
|
||||
};
|
||||
|
||||
const TAXON_FIELDS = {
|
||||
default_photo: {
|
||||
square_url: true
|
||||
},
|
||||
iconic_taxon_name: true,
|
||||
name: true,
|
||||
preferred_common_name: true,
|
||||
rank: true,
|
||||
rank_level: true
|
||||
};
|
||||
|
||||
const ID_FIELDS = {
|
||||
body: true,
|
||||
category: true,
|
||||
created_at: true,
|
||||
current: true,
|
||||
disagreement: true,
|
||||
taxon: TAXON_FIELDS,
|
||||
updated_at: true,
|
||||
user: Object.assign( { }, USER_FIELDS, { id: true } ),
|
||||
uuid: true,
|
||||
vision: true
|
||||
};
|
||||
|
||||
const PHOTO_FIELDS = {
|
||||
id: true,
|
||||
attribution: true,
|
||||
license_code: true,
|
||||
url: true
|
||||
};
|
||||
|
||||
const COMMENT_FIELDS = {
|
||||
body: true,
|
||||
created_at: true,
|
||||
id: true,
|
||||
user: USER_FIELDS
|
||||
};
|
||||
|
||||
const OBSERVATION_PHOTOS_FIELDS = {
|
||||
id: true,
|
||||
photo: PHOTO_FIELDS,
|
||||
position: true,
|
||||
uuid: true
|
||||
};
|
||||
|
||||
const FIELDS = {
|
||||
comments_count: true,
|
||||
comments: COMMENT_FIELDS,
|
||||
created_at: true,
|
||||
description: true,
|
||||
geojson: true,
|
||||
identifications: ID_FIELDS,
|
||||
latitude: true,
|
||||
location: true,
|
||||
longitude: true,
|
||||
observation_photos: OBSERVATION_PHOTOS_FIELDS,
|
||||
photos: PHOTO_FIELDS,
|
||||
place_guess: true,
|
||||
quality_grade: true,
|
||||
taxon: TAXON_FIELDS,
|
||||
time_observed_at: true,
|
||||
user: USER_FIELDS
|
||||
};
|
||||
|
||||
const useFetchObservations = ( ): boolean => {
|
||||
const [loading, setLoading] = useState( false );
|
||||
const realmRef = useRef( null );
|
||||
const subscriptionRef = useRef( null );
|
||||
|
||||
const openRealm = useCallback( async ( ) => {
|
||||
try {
|
||||
const realm = await Realm.open( realmConfig );
|
||||
realmRef.current = realm;
|
||||
|
||||
const localObservations = realm.objects( "Observation" );
|
||||
if ( localObservations?.length ) {
|
||||
setObservations( localObservations );
|
||||
}
|
||||
subscriptionRef.current = localObservations;
|
||||
}
|
||||
catch ( err ) {
|
||||
console.error( "Error opening realm: ", err.message );
|
||||
}
|
||||
}, [realmRef, setObservations] );
|
||||
}, [realmRef] );
|
||||
|
||||
const closeRealm = useCallback( ( ) => {
|
||||
const subscription = subscriptionRef.current;
|
||||
subscription?.removeAllListeners( );
|
||||
subscriptionRef.current = null;
|
||||
|
||||
const realm = realmRef.current;
|
||||
realm?.close( );
|
||||
realmRef.current = null;
|
||||
setObservations( [] );
|
||||
}, [realmRef] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
@@ -45,194 +106,34 @@ const useFetchObservations = ( ): Array<Object> => {
|
||||
return closeRealm;
|
||||
}, [openRealm, closeRealm] );
|
||||
|
||||
const FIELDS = useMemo( ( ) => {
|
||||
const USER_FIELDS = {
|
||||
icon_url: true,
|
||||
id: true,
|
||||
login: true,
|
||||
name: true
|
||||
};
|
||||
|
||||
const TAXON_FIELDS = {
|
||||
default_photo: {
|
||||
square_url: true
|
||||
},
|
||||
iconic_taxon_name: true,
|
||||
name: true,
|
||||
preferred_common_name: true,
|
||||
rank: true,
|
||||
rank_level: true
|
||||
};
|
||||
|
||||
const ID_FIELDS = {
|
||||
body: true,
|
||||
category: true,
|
||||
created_at: true,
|
||||
current: true,
|
||||
disagreement: true,
|
||||
taxon: TAXON_FIELDS,
|
||||
updated_at: true,
|
||||
user: Object.assign( { }, USER_FIELDS, { id: true } ),
|
||||
uuid: true,
|
||||
vision: true
|
||||
};
|
||||
|
||||
const PHOTO_FIELDS = {
|
||||
id: true,
|
||||
attribution: true,
|
||||
license_code: true,
|
||||
url: true
|
||||
};
|
||||
|
||||
const COMMENT_FIELDS = {
|
||||
body: true,
|
||||
created_at: true,
|
||||
id: true,
|
||||
user: USER_FIELDS
|
||||
};
|
||||
|
||||
return {
|
||||
comments_count: true,
|
||||
comments: COMMENT_FIELDS,
|
||||
created_at: true,
|
||||
description: true,
|
||||
geojson: true,
|
||||
identifications: ID_FIELDS,
|
||||
latitude: true,
|
||||
location: true,
|
||||
longitude: true,
|
||||
photos: PHOTO_FIELDS,
|
||||
place_guess: true,
|
||||
quality_grade: true,
|
||||
taxon: TAXON_FIELDS,
|
||||
time_observed_at: true,
|
||||
user: USER_FIELDS
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const createPhotoForRealm = ( photo ) => ( {
|
||||
id: photo.id,
|
||||
attribution: photo.attribution,
|
||||
licenseCode: photo.license_code,
|
||||
url: photo.url
|
||||
} );
|
||||
|
||||
const createIdentificationForRealm = ( id ) => ( {
|
||||
uuid: id.uuid,
|
||||
body: id.body,
|
||||
category: id.category,
|
||||
commonName: id.taxon.preferred_common_name,
|
||||
createdAt: id.created_at,
|
||||
id: id.id,
|
||||
name: id.taxon.name,
|
||||
rank: id.taxon.rank,
|
||||
taxonPhoto: id.taxon.default_photo.square_url,
|
||||
userIcon: id.user.icon_url,
|
||||
userLogin: id.user.login,
|
||||
vision: id.vision
|
||||
} );
|
||||
|
||||
const createCommentForRealm = ( id ) => ( {
|
||||
body: id.body,
|
||||
createdAt: id.created_at,
|
||||
id: id.id,
|
||||
user: id.user.login
|
||||
} );
|
||||
|
||||
const createLinkedIdentifications = useCallback( ( obs ) => {
|
||||
const identifications = [];
|
||||
|
||||
if ( obs.identifications.length > 0 ) {
|
||||
obs.identifications.forEach( ( id ) => {
|
||||
const linkedIdentification = createIdentificationForRealm( id );
|
||||
identifications.push( linkedIdentification );
|
||||
} );
|
||||
}
|
||||
return identifications;
|
||||
}, [] );
|
||||
|
||||
const createLinkedPhotos = useCallback( ( obs ) => {
|
||||
const photos = [];
|
||||
|
||||
if ( obs.photos.length > 0 ) {
|
||||
obs.photos.forEach( ( photo ) => {
|
||||
const linkedPhoto = createPhotoForRealm( photo );
|
||||
photos.push( linkedPhoto );
|
||||
} );
|
||||
}
|
||||
return photos;
|
||||
}, [] );
|
||||
|
||||
const createLinkedComments = useCallback( ( obs ) => {
|
||||
const comments = [];
|
||||
|
||||
if ( obs.comments.length > 0 ) {
|
||||
obs.comments.forEach( ( photo ) => {
|
||||
const linkedComment = createCommentForRealm( photo );
|
||||
comments.push( linkedComment );
|
||||
} );
|
||||
}
|
||||
return comments;
|
||||
}, [] );
|
||||
|
||||
const createObservationForRealm = useCallback( ( obs ) => {
|
||||
const identifications = createLinkedIdentifications( obs );
|
||||
const photos = createLinkedPhotos( obs );
|
||||
const comments = createLinkedComments( obs );
|
||||
|
||||
return {
|
||||
uuid: obs.uuid,
|
||||
commentCount: obs.comment_count || 0,
|
||||
comments,
|
||||
commonName: obs.taxon.preferred_common_name,
|
||||
createdAt: obs.created_at,
|
||||
description: obs.description,
|
||||
identificationCount: obs.identifications.length,
|
||||
identifications,
|
||||
// obs detail on web says geojson coords are preferred over lat/long
|
||||
// https://github.com/inaturalist/inaturalist/blob/df6572008f60845b8ef5972a92a9afbde6f67829/app/webpack/observations/show/ducks/observation.js#L145
|
||||
latitude: obs.geojson.coordinates[1],
|
||||
location: obs.location,
|
||||
longitude: obs.geojson.coordinates[0],
|
||||
photos,
|
||||
placeGuess: obs.place_guess,
|
||||
qualityGrade: obs.quality_grade,
|
||||
taxonRank: obs.taxon.rank,
|
||||
timeObservedAt: obs.time_observed_at,
|
||||
userProfilePhoto: obs.user.icon_url,
|
||||
userLogin: obs.user.login,
|
||||
userPhoto: obs.photos[0].url
|
||||
};
|
||||
}, [createLinkedIdentifications, createLinkedPhotos, createLinkedComments] );
|
||||
|
||||
const writeToDatabase = useCallback( ( results ) => {
|
||||
if ( results.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
// Everything in the function passed to "realm.write" is a transaction and will
|
||||
// hence succeed or fail together. A transcation is the smallest unit of transfer
|
||||
// in Realm so we want to be mindful of how much we put into one single transaction
|
||||
// and split them up if appropriate (more commonly seen server side). Since clients
|
||||
// may occasionally be online during short time spans we want to increase the probability
|
||||
// of sync participants to successfully sync everything in the transaction, otherwise
|
||||
// no changes propagate and the transaction needs to start over when connectivity allows.
|
||||
if ( results.length === 0 ) { return; }
|
||||
const realm = realmRef.current;
|
||||
results.forEach( obs => {
|
||||
const newObs = createObservationForRealm( obs );
|
||||
const newObs = Observation.createObservationForRealm( obs, realm );
|
||||
realm?.write( ( ) => {
|
||||
// Shouldn't the primary key in realm handle this?
|
||||
const existingObs = realm.objects( "Observation" ).filtered( `uuid = '${obs.uuid}'` );
|
||||
if ( existingObs.length > 0 ) {
|
||||
const existingObs = realm.objectForPrimaryKey( "Observation", obs.uuid );
|
||||
if ( existingObs !== undefined ) {
|
||||
// TODO: modify existing objects when syncing from inatjs
|
||||
return;
|
||||
}
|
||||
realm?.create( "Observation", newObs );
|
||||
// need to append Taxon object to identifications after the Observation object
|
||||
// has been created with its own Taxon object, otherwise will run into errors
|
||||
// with realm trying to create a Taxon object with an existing primary key
|
||||
obs.identifications.forEach( id => {
|
||||
const identification = realm.objectForPrimaryKey( "Identification", id.uuid );
|
||||
identification.taxon = Taxon.mapApiToRealm( id.taxon, realm );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
}, [createObservationForRealm] );
|
||||
setLoading( false );
|
||||
}, [] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
let isCurrent = true;
|
||||
const fetchObservations = async ( ) => {
|
||||
setLoading( true );
|
||||
try {
|
||||
const testUser = "albullington";
|
||||
const params = {
|
||||
@@ -245,6 +146,7 @@ const writeToDatabase = useCallback( ( results ) => {
|
||||
if ( !isCurrent ) { return; }
|
||||
writeToDatabase( results );
|
||||
} catch ( e ) {
|
||||
setLoading( false );
|
||||
if ( !isCurrent ) { return; }
|
||||
console.log( "Couldn't fetch observations:", e.message, );
|
||||
}
|
||||
@@ -254,9 +156,9 @@ const writeToDatabase = useCallback( ( results ) => {
|
||||
return ( ) => {
|
||||
isCurrent = false;
|
||||
};
|
||||
}, [FIELDS, writeToDatabase] );
|
||||
}, [writeToDatabase] );
|
||||
|
||||
return observations;
|
||||
return loading;
|
||||
};
|
||||
|
||||
export default useFetchObservations;
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import User from "./User";
|
||||
class Comment {
|
||||
static mapApiToRealm( comment, realm ) {
|
||||
return {
|
||||
body: comment.body,
|
||||
createdAt: comment.created_at,
|
||||
id: comment.id,
|
||||
user: User.mapApiToRealm( comment.user, realm )
|
||||
};
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "Comment",
|
||||
properties: {
|
||||
body: "string",
|
||||
createdAt: "string",
|
||||
id: "int",
|
||||
user: "string",
|
||||
body: "string?",
|
||||
createdAt: "string?",
|
||||
id: "int?",
|
||||
user: "User?",
|
||||
// this creates an inverse relationship so comments
|
||||
// automatically keep track of which Observation they are assigned to
|
||||
assignee: {
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import User from "./User";
|
||||
class Identification {
|
||||
static mapApiToRealm( id, realm ) {
|
||||
return {
|
||||
uuid: id.uuid,
|
||||
body: id.body,
|
||||
category: id.category,
|
||||
createdAt: id.created_at,
|
||||
id: id.id,
|
||||
user: User.mapApiToRealm( id.user, realm ),
|
||||
vision: id.vision
|
||||
};
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "Identification",
|
||||
primaryKey: "uuid",
|
||||
properties: {
|
||||
uuid: "string",
|
||||
body: "string?",
|
||||
category: "string",
|
||||
commonName: "string?",
|
||||
createdAt: "string",
|
||||
name: "string",
|
||||
rank: "string",
|
||||
taxonPhoto: "string",
|
||||
userIcon: "string?",
|
||||
userLogin: "string",
|
||||
vision: "bool",
|
||||
category: "string?",
|
||||
createdAt: "string?",
|
||||
taxon: "Taxon?",
|
||||
user: "User?",
|
||||
vision: "bool?",
|
||||
// this creates an inverse relationship so identifications
|
||||
// automatically keep track of which Observation they are assigned to
|
||||
assignee: {
|
||||
|
||||
@@ -1,27 +1,57 @@
|
||||
import Comment from "./Comment";
|
||||
import Identification from "./Identification";
|
||||
import ObservationPhoto from "./ObservationPhoto";
|
||||
import Taxon from "./Taxon";
|
||||
|
||||
class Observation {
|
||||
static createObservationForRealm( obs, realm ) {
|
||||
const createLinkedObjects = ( list, createFunction ) => {
|
||||
if ( list.length === 0 ) { return; }
|
||||
return list.map( item => {
|
||||
return createFunction.mapApiToRealm( item, realm, "obs" );
|
||||
} );
|
||||
};
|
||||
|
||||
const taxon = Taxon.mapApiToRealm( obs.taxon, realm );
|
||||
const observationPhotos = createLinkedObjects( obs.observation_photos, ObservationPhoto );
|
||||
const comments = createLinkedObjects( obs.comments, Comment, realm );
|
||||
const identifications = createLinkedObjects( obs.identifications, Identification );
|
||||
|
||||
return {
|
||||
uuid: obs.uuid,
|
||||
comments,
|
||||
createdAt: obs.created_at,
|
||||
description: obs.description,
|
||||
identifications,
|
||||
// obs detail on web says geojson coords are preferred over lat/long
|
||||
// https://github.com/inaturalist/inaturalist/blob/df6572008f60845b8ef5972a92a9afbde6f67829/app/webpack/observations/show/ducks/observation.js#L145
|
||||
latitude: obs.geojson.coordinates[1],
|
||||
longitude: obs.geojson.coordinates[0],
|
||||
observationPhotos,
|
||||
// photos,
|
||||
placeGuess: obs.place_guess,
|
||||
qualityGrade: obs.quality_grade,
|
||||
taxon,
|
||||
timeObservedAt: obs.time_observed_at
|
||||
};
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "Observation",
|
||||
primaryKey: "uuid",
|
||||
properties: {
|
||||
uuid: "string",
|
||||
commentCount: "int",
|
||||
comments: "Comment[]",
|
||||
commonName: "string?",
|
||||
createdAt: "string",
|
||||
createdAt: "string?",
|
||||
description: "string?",
|
||||
identifications: "Identification[]",
|
||||
identificationCount: "int",
|
||||
latitude: "double?",
|
||||
location: "string",
|
||||
longitude: "double?",
|
||||
photos: "Photo[]",
|
||||
placeGuess: "string",
|
||||
qualityGrade: "string",
|
||||
taxonRank: "string",
|
||||
timeObservedAt: "string",
|
||||
userProfilePhoto: "string",
|
||||
userLogin: "string",
|
||||
userPhoto: "string"
|
||||
observationPhotos: "ObservationPhoto[]",
|
||||
placeGuess: "string?",
|
||||
qualityGrade: "string?",
|
||||
taxon: "Taxon?",
|
||||
timeObservedAt: "string?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
src/models/ObservationPhoto.js
Normal file
32
src/models/ObservationPhoto.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import Photo from "./Photo";
|
||||
|
||||
class ObservationPhoto {
|
||||
static mapApiToRealm( observationPhoto ) {
|
||||
return {
|
||||
id: observationPhoto.id,
|
||||
position: observationPhoto.position,
|
||||
photo: Photo.mapApiToRealm( observationPhoto.photo ),
|
||||
uuid: observationPhoto.uuid
|
||||
};
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "ObservationPhoto",
|
||||
primaryKey: "uuid",
|
||||
properties: {
|
||||
uuid: "string",
|
||||
id: "int?",
|
||||
photo: "Photo?",
|
||||
position: "int?",
|
||||
// this creates an inverse relationship so observation photos
|
||||
// automatically keep track of which Observation they are assigned to
|
||||
assignee: {
|
||||
type: "linkingObjects",
|
||||
objectType: "Observation",
|
||||
property: "observationPhotos"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ObservationPhoto;
|
||||
@@ -1,19 +1,21 @@
|
||||
class Photo {
|
||||
static mapApiToRealm( photo ) {
|
||||
return {
|
||||
id: photo.id,
|
||||
attribution: photo.attribution,
|
||||
licenseCode: photo.license_code,
|
||||
url: photo.url
|
||||
};
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "Photo",
|
||||
// need uuid to be primary key for photos that get uploaded?
|
||||
properties: {
|
||||
id: "int",
|
||||
attribution: "string",
|
||||
licenseCode: "string",
|
||||
url: "string",
|
||||
// this creates an inverse relationship so photos
|
||||
// automatically keep track of which Observation they are assigned to
|
||||
assignee: {
|
||||
type: "linkingObjects",
|
||||
objectType: "Observation",
|
||||
property: "photos"
|
||||
}
|
||||
id: "int?",
|
||||
attribution: "string?",
|
||||
licenseCode: "string?",
|
||||
url: "string?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
src/models/Taxon.js
Normal file
27
src/models/Taxon.js
Normal file
@@ -0,0 +1,27 @@
|
||||
class Taxon {
|
||||
static mapApiToRealm( taxon, realm ) {
|
||||
const existingTaxon = realm.objectForPrimaryKey( "Taxon", taxon.id );
|
||||
if ( existingTaxon ) { return existingTaxon; }
|
||||
return {
|
||||
defaultPhotoSquareUrl: taxon.default_photo.square_url,
|
||||
id: taxon.id,
|
||||
name: taxon.name,
|
||||
preferredCommonName: taxon.preferred_common_name,
|
||||
rank: taxon.rank
|
||||
};
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "Taxon",
|
||||
primaryKey: "id",
|
||||
properties: {
|
||||
id: "int",
|
||||
defaultPhotoSquareUrl: "string?",
|
||||
name: "string?",
|
||||
preferredCommonName: "string?",
|
||||
rank: "string?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Taxon;
|
||||
25
src/models/User.js
Normal file
25
src/models/User.js
Normal file
@@ -0,0 +1,25 @@
|
||||
class User {
|
||||
static mapApiToRealm( user, realm ) {
|
||||
const existingUser = realm.objectForPrimaryKey( "User", user.id );
|
||||
if ( existingUser ) { return existingUser; }
|
||||
return {
|
||||
id: user.id,
|
||||
iconUrl: user.icon_url,
|
||||
login: user.login,
|
||||
name: user.name
|
||||
};
|
||||
}
|
||||
|
||||
static schema = {
|
||||
name: "User",
|
||||
primaryKey: "id",
|
||||
properties: {
|
||||
id: "int",
|
||||
iconUrl: "string?",
|
||||
login: "string?",
|
||||
name: "string?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
@@ -3,15 +3,21 @@
|
||||
import Comment from "./Comment";
|
||||
import Identification from "./Identification";
|
||||
import Observation from "./Observation";
|
||||
import ObservationPhoto from "./ObservationPhoto";
|
||||
import Photo from "./Photo";
|
||||
import Taxon from "./Taxon";
|
||||
import User from "./User";
|
||||
|
||||
export default {
|
||||
schema: [
|
||||
Comment,
|
||||
Identification,
|
||||
Observation,
|
||||
Photo
|
||||
ObservationPhoto,
|
||||
Photo,
|
||||
Taxon,
|
||||
User
|
||||
],
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
path: "db.realm"
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { createDrawerNavigator } from "@react-navigation/drawer";
|
||||
|
||||
import ObsList from "../components/Observations/ObsList";
|
||||
import PlaceholderComponent from "../components/PlaceholderComponent";
|
||||
import ObsListRightHeader from "../components/Observations/ObsListRightHeader";
|
||||
|
||||
// this removes the default hamburger menu from header
|
||||
const screenOptions = { headerLeft: ( ) => <></> };
|
||||
|
||||
const Drawer = createDrawerNavigator();
|
||||
|
||||
const DrawerNavigator = ( ): React.Node => (
|
||||
<Drawer.Navigator screenOptions={screenOptions}>
|
||||
<Drawer.Screen
|
||||
name="my observations"
|
||||
component={ObsList}
|
||||
options={( { navigation } ) => ( {
|
||||
headerRight: ( ) => <ObsListRightHeader />
|
||||
} )}
|
||||
/>
|
||||
<Drawer.Screen name="missions/seen nearby" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="search" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="identify" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="following (dashboard)" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="impact" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="projects" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="guides" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="about" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="help/tutorials" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="settings" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="logout" component={PlaceholderComponent} />
|
||||
</Drawer.Navigator>
|
||||
);
|
||||
|
||||
export default DrawerNavigator;
|
||||
49
src/navigation/myObservationsStackNavigation.js
Normal file
49
src/navigation/myObservationsStackNavigation.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { HeaderBackButton } from "@react-navigation/elements";
|
||||
|
||||
import ObsList from "../components/Observations/ObsList";
|
||||
import ObsDetails from "../components/ObsDetails/ObsDetails";
|
||||
import UserProfile from "../components/UserProfile/UserProfile";
|
||||
import MessagesIcon from "../components/Observations/MessagesIcon";
|
||||
import ObservationProvider from "../providers/ObservationProvider";
|
||||
|
||||
const Stack = createNativeStackNavigator( );
|
||||
|
||||
const screenOptions = {
|
||||
headerShown: true
|
||||
};
|
||||
|
||||
const showBackButton = ( { navigation } ) => ( {
|
||||
headerLeft: ( ) => <HeaderBackButton onPress={( ) => navigation.goBack( )} />
|
||||
} );
|
||||
|
||||
const App = ( ): React.Node => (
|
||||
// TODO: decide whether ObservationProvider needs to wrap both ObsList and ObsDetail
|
||||
// or whether we should simply pass the uuid through navigation params
|
||||
<ObservationProvider>
|
||||
<Stack.Navigator screenOptions={screenOptions}>
|
||||
<Stack.Screen
|
||||
name="ObsList"
|
||||
component={ObsList}
|
||||
options={( { navigation } ) => ( {
|
||||
headerRight: ( ) => <MessagesIcon />
|
||||
} )}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ObsDetails"
|
||||
component={ObsDetails}
|
||||
options={showBackButton}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="UserProfile"
|
||||
component={UserProfile}
|
||||
options={showBackButton}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</ObservationProvider>
|
||||
);
|
||||
|
||||
export default App;
|
||||
43
src/navigation/rootNavigation.js
Normal file
43
src/navigation/rootNavigation.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { createDrawerNavigator } from "@react-navigation/drawer";
|
||||
|
||||
import PlaceholderComponent from "../components/PlaceholderComponent";
|
||||
import MyObservationsStackNavigator from "./myObservationsStackNavigation";
|
||||
|
||||
// this removes the default hamburger menu from header
|
||||
const screenOptions = { headerLeft: ( ) => <></> };
|
||||
const hideHeader = {
|
||||
headerShown: false,
|
||||
label: "my observations"
|
||||
};
|
||||
|
||||
const Drawer = createDrawerNavigator( );
|
||||
|
||||
const App = ( ): React.Node => (
|
||||
<NavigationContainer>
|
||||
<Drawer.Navigator screenOptions={screenOptions} name="Drawer">
|
||||
<Drawer.Screen
|
||||
name="my observations"
|
||||
component={MyObservationsStackNavigator}
|
||||
options={hideHeader}
|
||||
/>
|
||||
<Drawer.Screen name="missions/seen nearby" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="search" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="identify" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="following (dashboard)" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="impact" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="projects" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="guides" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="about" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="help/tutorials" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="settings" component={PlaceholderComponent} />
|
||||
<Drawer.Screen name="logout" component={PlaceholderComponent} />
|
||||
</Drawer.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
|
||||
import DrawerNavigator from "./drawerNavigation";
|
||||
import ObsDetails from "../components/ObsDetails/ObsDetails";
|
||||
import UserProfile from "../components/UserProfile/UserProfile";
|
||||
import ObservationProvider from "../providers/ObservationProvider";
|
||||
|
||||
const Stack = createNativeStackNavigator( );
|
||||
|
||||
const screenOptions = { headerShown: false };
|
||||
|
||||
const App = ( ): React.Node => (
|
||||
<NavigationContainer>
|
||||
<ObservationProvider>
|
||||
<Stack.Navigator screenOptions={screenOptions}>
|
||||
<Stack.Screen name="Drawer" component={DrawerNavigator} />
|
||||
<Stack.Screen name="ObsDetails" component={ObsDetails} options={{ headerShown: true }} />
|
||||
<Stack.Screen name="UserProfile" component={UserProfile} options={{ headerShown: true }} />
|
||||
</Stack.Navigator>
|
||||
</ObservationProvider>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -32,6 +32,9 @@ const ObservationProvider = ( { children }: Props ): Node => {
|
||||
}
|
||||
|
||||
localObservations.addListener( ( ) => {
|
||||
// changes object has properties including insertions, modifications, and deletions
|
||||
// so we can decide when we need obslist to rerender here. otherwise, it will listen for all changes
|
||||
|
||||
// If you just pass localObservations you end up assigning a Results
|
||||
// object to state instead of an array of observations. There's
|
||||
// probably a better way...
|
||||
|
||||
8
tests/factories/LocalComment.js
Normal file
8
tests/factories/LocalComment.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import factory, { define } from "factoria";
|
||||
|
||||
export default define( "LocalComment", faker => ( {
|
||||
id: faker.datatype.number( ),
|
||||
createdAt: "2019-09-09T08:28:05-08:00",
|
||||
user: factory( "RemoteUser" ),
|
||||
body: faker.lorem.sentence( )
|
||||
} ) );
|
||||
7
tests/factories/LocalIdentification.js
Normal file
7
tests/factories/LocalIdentification.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { define } from "factoria";
|
||||
|
||||
export default define( "LocalIdentification", faker => ( {
|
||||
uuid: faker.datatype.number( ),
|
||||
createdAt: "2017-09-09T08:28:05-08:00",
|
||||
body: faker.lorem.sentence( )
|
||||
} ) );
|
||||
@@ -1,14 +1,21 @@
|
||||
import { define } from "factoria";
|
||||
import factory, { define } from "factoria";
|
||||
|
||||
// TODO use faker for more of these dynamic values. Also, would we really
|
||||
// store timeObservedAt like this in Realm?!
|
||||
// TODO use faker for more of these dynamic values.
|
||||
export default define( "LocalObservation", faker => ( {
|
||||
uuid: faker.datatype.uuid( ),
|
||||
userPhoto: faker.image.imageUrl( ),
|
||||
commonName: "Insects",
|
||||
comments: [
|
||||
factory( "LocalComment" ),
|
||||
factory( "LocalComment" ),
|
||||
factory( "LocalComment" )
|
||||
],
|
||||
identifications: [
|
||||
factory( "LocalIdentification" )
|
||||
],
|
||||
observationPhotos: [
|
||||
factory( "LocalObservationPhoto" )
|
||||
],
|
||||
placeGuess: "SF",
|
||||
timeObservedAt: "May 1, 2021",
|
||||
identificationCount: 3,
|
||||
commentCount: 0,
|
||||
taxon: factory( "LocalTaxon" ),
|
||||
timeObservedAt: "2021-05-09T07:27:05-06:00",
|
||||
qualityGrade: "research"
|
||||
} ) );
|
||||
|
||||
8
tests/factories/LocalObservationPhoto.js
Normal file
8
tests/factories/LocalObservationPhoto.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import factory, { define } from "factoria";
|
||||
|
||||
export default define( "LocalObservationPhoto", faker => ( {
|
||||
uuid: faker.datatype.uuid( ),
|
||||
id: faker.datatype.number( ),
|
||||
photo: factory( "LocalPhoto" ),
|
||||
position: faker.datatype.number( )
|
||||
} ) );
|
||||
8
tests/factories/LocalPhoto.js
Normal file
8
tests/factories/LocalPhoto.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { define } from "factoria";
|
||||
|
||||
export default define( "LocalPhoto", faker => ( {
|
||||
id: faker.datatype.number( ),
|
||||
attribution: faker.lorem.sentence( ),
|
||||
licenseCode: "cc-by-nc",
|
||||
url: faker.image.imageUrl( )
|
||||
} ) );
|
||||
8
tests/factories/LocalTaxon.js
Normal file
8
tests/factories/LocalTaxon.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { define } from "factoria";
|
||||
|
||||
export default define( "LocalTaxon", faker => ( {
|
||||
id: faker.datatype.number( ),
|
||||
name: faker.name.firstName( ),
|
||||
rank: "family",
|
||||
preferredCommonName: faker.name.findName( )
|
||||
} ) );
|
||||
@@ -8,8 +8,8 @@ export default define( "RemoteObservation", faker => ( {
|
||||
uuid: faker.datatype.uuid( ),
|
||||
user: factory( "RemoteUser" ),
|
||||
identifications: [],
|
||||
photos: [
|
||||
factory( "RemotePhoto" )
|
||||
observation_photos: [
|
||||
factory( "RemoteObservationPhoto" )
|
||||
],
|
||||
comments: [],
|
||||
taxon: factory( "RemoteTaxon" ),
|
||||
|
||||
8
tests/factories/RemoteObservationPhoto.js
Normal file
8
tests/factories/RemoteObservationPhoto.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import factory, { define } from "factoria";
|
||||
|
||||
export default define( "RemoteObservationPhoto", faker => ( {
|
||||
uuid: faker.datatype.uuid( ),
|
||||
id: faker.datatype.number( ),
|
||||
photo: factory( "RemotePhoto" ),
|
||||
position: faker.datatype.number( )
|
||||
} ) );
|
||||
@@ -1,7 +1,11 @@
|
||||
import { define } from "factoria";
|
||||
|
||||
export default define( "RemoteTaxon", faker => ( {
|
||||
id: faker.datatype.number( ),
|
||||
name: faker.name.firstName( ),
|
||||
rank: "genus",
|
||||
preferred_common_name: faker.name.findName( )
|
||||
preferred_common_name: faker.name.findName( ),
|
||||
default_photo: {
|
||||
square_url: faker.image.imageUrl( )
|
||||
}
|
||||
} ) );
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React from "react";
|
||||
import factory, { makeResponse } from "../factory";
|
||||
import { render, waitFor, within, act } from "@testing-library/react-native";
|
||||
import { render, waitFor, within } from "@testing-library/react-native";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import AccessibilityEngine from "react-native-accessibility-engine";
|
||||
import ObsList from "../../src/components/Observations/ObsList";
|
||||
@@ -24,7 +24,9 @@ const renderObsList = async ( ) => waitFor(
|
||||
);
|
||||
|
||||
test( "renders the number of comments from remote response", async ( ) => {
|
||||
const observations = [factory( "RemoteObservation", { place_guess: "foo", comment_count: 13 } )];
|
||||
const observations = [factory( "RemoteObservation", { place_guess: "foo", comments: [
|
||||
factory( "LocalComment" )
|
||||
] } )];
|
||||
inatjs.observations.search.mockResolvedValue( makeResponse( observations ) );
|
||||
const { getByTestId } = await renderObsList( );
|
||||
expect( inatjs.observations.search.mock.calls.length ).toBeGreaterThan( 0 );
|
||||
@@ -32,7 +34,7 @@ test( "renders the number of comments from remote response", async ( ) => {
|
||||
const card = getByTestId( `ObsList.obsCard.${obs.uuid}` );
|
||||
expect( card ).toBeTruthy( );
|
||||
const commentCount = within( card ).getByTestId( "ObsList.obsCard.commentCount" );
|
||||
expect( commentCount.children[0] ).toEqual( obs.comment_count.toString( ) );
|
||||
expect( commentCount.children[0] ).toEqual( obs.comments.length.toString( ) );
|
||||
} );
|
||||
|
||||
test.todo( "only makes one concurrent request for observations at a time" );
|
||||
|
||||
@@ -14,12 +14,12 @@ test( "renders text passed into observation card", ( ) => {
|
||||
);
|
||||
|
||||
expect( getByTestId( `ObsList.obsCard.${testObservation.uuid}` ) ).toBeTruthy( );
|
||||
expect( getByTestId( "ObsList.photo" ).props.source ).toStrictEqual( { "uri": testObservation.userPhoto } );
|
||||
expect( getByText( testObservation.commonName ) ).toBeTruthy( );
|
||||
expect( getByTestId( "ObsList.photo" ).props.source ).toStrictEqual( { "uri": testObservation.observationPhotos[0].photo.url } );
|
||||
expect( getByText( testObservation.taxon.preferredCommonName ) ).toBeTruthy( );
|
||||
expect( getByText( testObservation.placeGuess ) ).toBeTruthy( );
|
||||
expect( getByText( testObservation.timeObservedAt ) ).toBeTruthy( );
|
||||
expect( getByText( testObservation.commentCount.toString( ) ) ).toBeTruthy( );
|
||||
expect( getByText( testObservation.identificationCount.toString( ) ) ).toBeTruthy( );
|
||||
expect( getByText( testObservation.comments.length.toString( ) ) ).toBeTruthy( );
|
||||
expect( getByText( testObservation.identifications.length.toString( ) ) ).toBeTruthy( );
|
||||
expect( getByText( testObservation.qualityGrade ) ).toBeTruthy( );
|
||||
} );
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { waitFor, render, within } from "@testing-library/react-native";
|
||||
import { render, within } from "@testing-library/react-native";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import factory from "../../../factory";
|
||||
import ObsList from "../../../../src/components/Observations/ObsList";
|
||||
@@ -35,8 +35,11 @@ const renderObsList = ( ) => render(
|
||||
);
|
||||
|
||||
it( "renders an observation", ( ) => {
|
||||
// const observations = [factory( "LocalObservation", { commentCount: 11 } )];
|
||||
const observations = [factory( "LocalObservation", { commentCount: 11 } )];
|
||||
const observations = [factory( "LocalObservation", { comments: [
|
||||
factory( "LocalComment" ),
|
||||
factory( "LocalComment" ),
|
||||
factory( "LocalComment" )
|
||||
] } )];
|
||||
// Mock the provided observations so we're just using our test data
|
||||
mockObservationProviderWithObservations( observations );
|
||||
const { getByTestId } = renderObsList( );
|
||||
@@ -49,7 +52,7 @@ it( "renders an observation", ( ) => {
|
||||
expect( card ).toBeTruthy( );
|
||||
// Test that the card has the correct comment count
|
||||
const commentCount = within( card ).getByTestId( "ObsList.obsCard.commentCount" );
|
||||
expect( commentCount.children[0] ).toEqual( obs.commentCount.toString( ) );
|
||||
expect( commentCount.children[0] ).toEqual( obs.comments.length.toString( ) );
|
||||
} );
|
||||
|
||||
it( "renders multiple observations", async ( ) => {
|
||||
|
||||
Reference in New Issue
Block a user