mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-06-19 13:11:23 -04:00
Merge branch 'main' into obs-detail
This commit is contained in:
@@ -60,7 +60,6 @@
|
||||
00E356EE1AD99517003FC87E /* iNaturalistReactNativeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iNaturalistReactNativeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
00E356F21AD99517003FC87E /* iNaturalistReactNativeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iNaturalistReactNativeTests.m; sourceTree = "<group>"; };
|
||||
0FDA07B1F0D135536388A8A9 /* libPods-iNaturalistReactNative.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iNaturalistReactNative.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07F961A680F5B00A75B9A /* iNaturalistReactNative.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iNaturalistReactNative.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = iNaturalistReactNative/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = iNaturalistReactNative/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.5",
|
||||
"@gorhom/bottom-sheet": "^4.4.6",
|
||||
"@gorhom/bottom-sheet": "^4.4.7",
|
||||
"@react-native-async-storage/async-storage": "^1.18.1",
|
||||
"@react-native-camera-roll/camera-roll": "^5.4.0",
|
||||
"@react-native-community/checkbox": "^0.5.15",
|
||||
@@ -2209,9 +2209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@gorhom/bottom-sheet": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.6.tgz",
|
||||
"integrity": "sha512-okqJPtFQjfqPZdh6wGDzQKkMevG1IfplQeoWY0VqOFCp3E0p7WHNeW41voK7KXXCVTQaGXibPfd9GNGjXgFNyg==",
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.7.tgz",
|
||||
"integrity": "sha512-ukTuTqDQi2heo68hAJsBpUQeEkdqP9REBcn47OpuvPKhdPuO1RBOOADjqXJNCnZZRcY+HqbnGPMSLFVc31zylQ==",
|
||||
"dependencies": {
|
||||
"@gorhom/portal": "1.0.14",
|
||||
"invariant": "^2.2.4"
|
||||
@@ -26893,9 +26893,9 @@
|
||||
"integrity": "sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ=="
|
||||
},
|
||||
"@gorhom/bottom-sheet": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.6.tgz",
|
||||
"integrity": "sha512-okqJPtFQjfqPZdh6wGDzQKkMevG1IfplQeoWY0VqOFCp3E0p7WHNeW41voK7KXXCVTQaGXibPfd9GNGjXgFNyg==",
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.7.tgz",
|
||||
"integrity": "sha512-ukTuTqDQi2heo68hAJsBpUQeEkdqP9REBcn47OpuvPKhdPuO1RBOOADjqXJNCnZZRcY+HqbnGPMSLFVc31zylQ==",
|
||||
"requires": {
|
||||
"@gorhom/portal": "1.0.14",
|
||||
"invariant": "^2.2.4"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.5",
|
||||
"@gorhom/bottom-sheet": "^4.4.6",
|
||||
"@gorhom/bottom-sheet": "^4.4.7",
|
||||
"@react-native-async-storage/async-storage": "^1.18.1",
|
||||
"@react-native-camera-roll/camera-roll": "^5.4.0",
|
||||
"@react-native-community/checkbox": "^0.5.15",
|
||||
|
||||
@@ -20,7 +20,7 @@ type Props = {
|
||||
currentUser: ?Object,
|
||||
numObservations: number,
|
||||
setHeightAboveToolbar: Function,
|
||||
uploadStatus: Object,
|
||||
allObsToUpload: Array<Object>,
|
||||
setShowLoginSheet: Function
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const Header = ( {
|
||||
currentUser,
|
||||
numObservations,
|
||||
setHeightAboveToolbar,
|
||||
uploadStatus,
|
||||
allObsToUpload,
|
||||
setShowLoginSheet
|
||||
}: Props ): Node => {
|
||||
const theme = useTheme( );
|
||||
@@ -118,7 +118,7 @@ const Header = ( {
|
||||
toggleLayout={toggleLayout}
|
||||
layout={layout}
|
||||
numUnuploadedObs={numUnuploadedObs}
|
||||
uploadStatus={uploadStatus}
|
||||
allObsToUpload={allObsToUpload}
|
||||
setShowLoginSheet={setShowLoginSheet}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Props = {
|
||||
observations: Array<Object>,
|
||||
onEndReached: Function,
|
||||
toggleLayout: Function,
|
||||
uploadStatus: Object,
|
||||
allObsToUpload: Array<Object>,
|
||||
currentUser: ?Object,
|
||||
showLoginSheet: boolean,
|
||||
setShowLoginSheet: Function,
|
||||
@@ -71,7 +71,7 @@ const MyObservations = ( {
|
||||
observations,
|
||||
onEndReached,
|
||||
toggleLayout,
|
||||
uploadStatus,
|
||||
allObsToUpload,
|
||||
currentUser,
|
||||
showLoginSheet,
|
||||
setShowLoginSheet
|
||||
@@ -164,7 +164,7 @@ const MyObservations = ( {
|
||||
observation={item}
|
||||
layout={layout}
|
||||
gridItemWidth={gridItemWidth}
|
||||
uploadStatus={uploadStatus}
|
||||
allObsToUpload={allObsToUpload}
|
||||
setShowLoginSheet={setShowLoginSheet}
|
||||
/>
|
||||
);
|
||||
@@ -215,12 +215,12 @@ const MyObservations = ( {
|
||||
currentUser={currentUser}
|
||||
numObservations={observations.length}
|
||||
setHeightAboveToolbar={setHeightAboveToolbar}
|
||||
uploadStatus={uploadStatus}
|
||||
allObsToUpload={allObsToUpload}
|
||||
setShowLoginSheet={setShowLoginSheet}
|
||||
/>
|
||||
<AnimatedFlashList
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
data={observations}
|
||||
data={observations.filter( o => o.isValid() )}
|
||||
key={layout}
|
||||
estimatedItemSize={
|
||||
layout === "grid"
|
||||
|
||||
@@ -7,15 +7,13 @@ import {
|
||||
useCurrentUser,
|
||||
useInfiniteScroll,
|
||||
useLocalObservations,
|
||||
useObservationsUpdates,
|
||||
useUploadObservations
|
||||
useObservationsUpdates
|
||||
} from "sharedHooks";
|
||||
|
||||
import MyObservations from "./MyObservations";
|
||||
|
||||
const MyObservationsContainer = ( ): Node => {
|
||||
const { observationList: observations, allObsToUpload } = useLocalObservations( );
|
||||
const uploadStatus = useUploadObservations( allObsToUpload );
|
||||
const { getItem, setItem } = useAsyncStorage( "myObservationsLayout" );
|
||||
const [layout, setLayout] = useState( null );
|
||||
const { isFetchingNextPage, fetchNextPage } = useInfiniteScroll( );
|
||||
@@ -56,7 +54,7 @@ const MyObservationsContainer = ( ): Node => {
|
||||
layout={layout}
|
||||
toggleLayout={toggleLayout}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
uploadStatus={uploadStatus}
|
||||
allObsToUpload={allObsToUpload}
|
||||
currentUser={currentUser}
|
||||
showLoginSheet={showLoginSheet}
|
||||
setShowLoginSheet={setShowLoginSheet}
|
||||
|
||||
@@ -32,12 +32,20 @@ const ObsUploadStatus = ( {
|
||||
const theme = useTheme( );
|
||||
const currentUser = useCurrentUser( );
|
||||
const obsEditContext = useContext( ObsEditContext );
|
||||
const startSingleUpload = obsEditContext?.startSingleUpload;
|
||||
const uploadObservation = obsEditContext?.uploadObservation;
|
||||
const uploadProgress = obsEditContext?.uploadProgress;
|
||||
const whiteColor = white && theme.colors.onPrimary;
|
||||
const isConnected = useIsConnected( );
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const needsSync = item => !item._synced_at
|
||||
|| item._synced_at <= item._updated_at;
|
||||
|
||||
const totalProgressIncrements = needsSync( observation )
|
||||
+ observation
|
||||
.observationPhotos.map( obsPhoto => needsSync( obsPhoto ) ).length;
|
||||
const currentProgress = uploadProgress?.[observation.uuid];
|
||||
|
||||
const displayUploadStatus = ( ) => {
|
||||
const obsStatus = (
|
||||
<ObsStatus
|
||||
@@ -48,12 +56,12 @@ const ObsUploadStatus = ( {
|
||||
/>
|
||||
);
|
||||
|
||||
const progress = uploadProgress?.[observation.uuid];
|
||||
if ( !observation.id || typeof progress === "number" ) {
|
||||
if ( !observation.id || typeof currentProgress === "number" ) {
|
||||
const progress = currentProgress / totalProgressIncrements;
|
||||
return (
|
||||
<UploadStatus
|
||||
progress={progress || 0}
|
||||
startSingleUpload={() => {
|
||||
uploadObservation={() => {
|
||||
if ( !isConnected ) {
|
||||
Alert.alert(
|
||||
t( "Internet-Connection-Required" ),
|
||||
@@ -66,7 +74,7 @@ const ObsUploadStatus = ( {
|
||||
setShowLoginSheet( true );
|
||||
return;
|
||||
}
|
||||
startSingleUpload( observation );
|
||||
uploadObservation( observation );
|
||||
}}
|
||||
color={whiteColor}
|
||||
completeColor={whiteColor}
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { ObsEditContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Dimensions, PixelRatio
|
||||
} from "react-native";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Alert, Dimensions, PixelRatio } from "react-native";
|
||||
import {
|
||||
useCurrentUser,
|
||||
useIsConnected,
|
||||
@@ -21,39 +18,38 @@ type Props = {
|
||||
toggleLayout: Function,
|
||||
layout: string,
|
||||
numUnuploadedObs: number,
|
||||
uploadStatus: Object,
|
||||
allObsToUpload: Array<Object>,
|
||||
setShowLoginSheet: Function
|
||||
}
|
||||
|
||||
const ToolbarContainer = ( {
|
||||
toggleLayout, layout, numUnuploadedObs,
|
||||
uploadStatus,
|
||||
allObsToUpload,
|
||||
setShowLoginSheet
|
||||
}: Props ): Node => {
|
||||
const { t } = useTranslation( );
|
||||
const currentUser = useCurrentUser( );
|
||||
const obsEditContext = useContext( ObsEditContext );
|
||||
const syncObservations = obsEditContext?.syncObservations;
|
||||
const stopUpload = obsEditContext?.stopUpload;
|
||||
const setUploadProgress = obsEditContext?.setUploadProgress;
|
||||
const uploadInProgress = obsEditContext?.uploadInProgress;
|
||||
const uploadMultipleObservations = obsEditContext?.uploadMultipleObservations;
|
||||
const currentUploadIndex = obsEditContext?.currentUploadIndex;
|
||||
const progress = obsEditContext?.progress;
|
||||
const setUploads = obsEditContext?.setUploads;
|
||||
const uploads = obsEditContext?.uploads;
|
||||
const uploadError = obsEditContext?.error;
|
||||
const navigation = useNavigation( );
|
||||
const isOnline = useIsConnected( );
|
||||
const {
|
||||
stopUpload,
|
||||
uploadInProgress,
|
||||
startUpload,
|
||||
progress,
|
||||
error: uploadError,
|
||||
currentUploadIndex,
|
||||
totalUploadCount
|
||||
} = uploadStatus;
|
||||
const uploadComplete = progress === 1;
|
||||
const [totalUploadCount, setTotalUploadCount] = useState( allObsToUpload?.length || 0 );
|
||||
|
||||
const screenWidth = Dimensions.get( "window" ).width * PixelRatio.get();
|
||||
|
||||
const syncObservations = obsEditContext?.syncObservations;
|
||||
|
||||
const { refetch } = useObservationsUpdates( false );
|
||||
|
||||
const getStatusText = ( ) => {
|
||||
if ( uploadComplete ) {
|
||||
if ( progress === 1 ) {
|
||||
return t( "X-observations-uploaded", { count: totalUploadCount } );
|
||||
}
|
||||
|
||||
@@ -93,7 +89,8 @@ const ToolbarContainer = ( {
|
||||
}
|
||||
|
||||
if ( numUnuploadedObs > 0 ) {
|
||||
startUpload( );
|
||||
setTotalUploadCount( allObsToUpload.length );
|
||||
setUploads( allObsToUpload );
|
||||
} else {
|
||||
syncObservations( );
|
||||
refetch( );
|
||||
@@ -108,15 +105,22 @@ const ToolbarContainer = ( {
|
||||
( numUnuploadedObs > 0 && !uploadInProgress ) || uploadError
|
||||
);
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( uploads?.length > 0 ) {
|
||||
uploadMultipleObservations( );
|
||||
}
|
||||
}, [uploads, uploadMultipleObservations] );
|
||||
|
||||
// clear upload status when leaving screen
|
||||
useEffect(
|
||||
( ) => {
|
||||
navigation.addListener( "blur", ( ) => {
|
||||
uploadStatus.stopUpload( );
|
||||
obsEditContext?.setUploadProgress( { } );
|
||||
stopUpload( );
|
||||
setUploadProgress( { } );
|
||||
setTotalUploadCount( 0 );
|
||||
} );
|
||||
},
|
||||
[navigation, uploadStatus, obsEditContext]
|
||||
[navigation, setUploadProgress, stopUpload]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,9 @@ import { formatISO } from "date-fns";
|
||||
import _ from "lodash";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, {
|
||||
useCallback, useEffect, useMemo, useState
|
||||
} from "react";
|
||||
import { Alert, LogBox } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -279,6 +281,18 @@ const ObsDetails = (): Node => {
|
||||
}
|
||||
}, [observation, comments] );
|
||||
|
||||
const editButton = useMemo( ( ) => (
|
||||
<IconButton
|
||||
onPress={navToObsEdit}
|
||||
icon="pencil"
|
||||
textColor={colors.white}
|
||||
className="absolute top-3 right-3"
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t( "edit" )}
|
||||
/>
|
||||
), [navToObsEdit, t] );
|
||||
|
||||
if ( !observation ) {
|
||||
return null;
|
||||
}
|
||||
@@ -381,15 +395,7 @@ const ObsDetails = (): Node => {
|
||||
<View className="bg-black">
|
||||
<PhotoScroll photos={photos} />
|
||||
{/* TODO: a11y props are not passed down into this 3.party */}
|
||||
<IconButton
|
||||
onPress={navToObsEdit}
|
||||
icon="pencil"
|
||||
textColor={colors.white}
|
||||
className="absolute top-3 right-3"
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t( "edit" )}
|
||||
/>
|
||||
{ editButton }
|
||||
{userFav
|
||||
? (
|
||||
<IconButton
|
||||
@@ -430,6 +436,7 @@ const ObsDetails = (): Node => {
|
||||
accessible
|
||||
accessibilityLabel={t( "Observation-has-no-photos-and-no-sounds" )}
|
||||
>
|
||||
{ editButton }
|
||||
<IconMaterial
|
||||
color={colors.white}
|
||||
testID="ObsDetails.noImage"
|
||||
|
||||
@@ -10,7 +10,7 @@ import React, {
|
||||
useContext,
|
||||
useEffect, useState
|
||||
} from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { ActivityIndicator, FlatList } from "react-native";
|
||||
import colors from "styles/tailwindColors";
|
||||
|
||||
type Props = {
|
||||
@@ -24,7 +24,8 @@ const EvidenceList = ( {
|
||||
}: Props ): Node => {
|
||||
const {
|
||||
setMediaViewerUris,
|
||||
setSelectedPhotoIndex
|
||||
setSelectedPhotoIndex,
|
||||
savingPhoto
|
||||
} = useContext( ObsEditContext );
|
||||
const navigation = useNavigation( );
|
||||
const [deletePhotoMode, setDeletePhotoMode] = useState( false );
|
||||
@@ -51,6 +52,19 @@ const EvidenceList = ( {
|
||||
);
|
||||
}
|
||||
|
||||
// add skeleton ActivityIndicator when a photo is being saved from the add evidence flow
|
||||
if ( item === "savingPhoto" ) {
|
||||
return (
|
||||
<View className={classnames( imageClass )}>
|
||||
<View className="rounded-lg overflow-hidden">
|
||||
<View className="bg-lightGray w-fit h-full justify-center">
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
@@ -75,6 +89,9 @@ const EvidenceList = ( {
|
||||
|
||||
const data = [...photoUris];
|
||||
data.unshift( "add" );
|
||||
if ( savingPhoto ) {
|
||||
data.push( "savingPhoto" );
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="mt-5">
|
||||
|
||||
@@ -59,7 +59,6 @@ const Header = ( ): Node => {
|
||||
|
||||
const handleBackButtonPress = useCallback( ( ) => {
|
||||
const unsyncedObservation = !currentObservation._synced_at && currentObservation._created_at;
|
||||
console.log( currentObservation, "current observation" );
|
||||
if ( params?.lastScreen === "GroupPhotos"
|
||||
|| ( unsyncedObservation && !unsavedChanges )
|
||||
) {
|
||||
|
||||
@@ -70,6 +70,7 @@ const TextInputSheet = ( {
|
||||
textAlignVertical: "top"
|
||||
}}
|
||||
autoFocus
|
||||
defaultValue={input}
|
||||
/>
|
||||
<Body3
|
||||
className="z-50 absolute bottom-20 right-5 p-5"
|
||||
|
||||
@@ -18,7 +18,7 @@ type Props = {
|
||||
color?: string,
|
||||
completeColor?: string,
|
||||
progress: number,
|
||||
startSingleUpload: Function,
|
||||
uploadObservation: Function,
|
||||
layout: string,
|
||||
children: any
|
||||
}
|
||||
@@ -43,7 +43,7 @@ const UploadStatus = ( {
|
||||
color,
|
||||
completeColor,
|
||||
progress,
|
||||
startSingleUpload,
|
||||
uploadObservation,
|
||||
layout,
|
||||
children
|
||||
}: Props ): Node => {
|
||||
@@ -88,9 +88,9 @@ const UploadStatus = ( {
|
||||
return t( "Upload-Complete" );
|
||||
};
|
||||
|
||||
const startUpload = () => {
|
||||
const startUpload = async ( ) => {
|
||||
startAnimation();
|
||||
startSingleUpload();
|
||||
await uploadObservation( );
|
||||
};
|
||||
|
||||
useEffect( () => () => cancelAnimation( rotation ), [rotation] );
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { activateKeepAwake, deactivateKeepAwake } from "@sayem314/react-native-keep-awake";
|
||||
import {
|
||||
activateKeepAwake,
|
||||
deactivateKeepAwake
|
||||
} from "@sayem314/react-native-keep-awake";
|
||||
import { searchObservations } from "api/observations";
|
||||
createObservation, createOrUpdateEvidence, searchObservations, updateObservation
|
||||
} from "api/observations";
|
||||
import inatjs from "inaturalistjs";
|
||||
import type { Node } from "react";
|
||||
import React, {
|
||||
useCallback, useEffect,
|
||||
@@ -15,10 +15,15 @@ import { EventRegister } from "react-native-event-listeners";
|
||||
import Observation from "realmModels/Observation";
|
||||
import ObservationPhoto from "realmModels/ObservationPhoto";
|
||||
import Photo from "realmModels/Photo";
|
||||
import emitUploadProgress, {
|
||||
INCREMENT_SINGLE_UPLOAD_PROGRESS
|
||||
} from "sharedHelpers/emitUploadProgress";
|
||||
import fetchPlaceName from "sharedHelpers/fetchPlaceName";
|
||||
import { formatExifDateAsString, parseExif, writeExifToFile } from "sharedHelpers/parseExif";
|
||||
import useApiToken from "sharedHooks/useApiToken";
|
||||
import useCurrentUser from "sharedHooks/useCurrentUser";
|
||||
import {
|
||||
useApiToken,
|
||||
useCurrentUser
|
||||
} from "sharedHooks";
|
||||
|
||||
import { log } from "../../react-native-logs.config";
|
||||
import { ObsEditContext, RealmContext } from "./contexts";
|
||||
@@ -31,11 +36,14 @@ type Props = {
|
||||
|
||||
const logger = log.extend( "ObsEditProvider" );
|
||||
|
||||
const uploadProgressIncrement = 0.5;
|
||||
|
||||
const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
const navigation = useNavigation( );
|
||||
const realm = useRealm( );
|
||||
const apiToken = useApiToken( );
|
||||
const currentUser = useCurrentUser( );
|
||||
// state related to creating/editing an observation
|
||||
const [currentObservationIndex, setCurrentObservationIndex] = useState( 0 );
|
||||
const [observations, setObservations] = useState( [] );
|
||||
const [cameraPreviewUris, setCameraPreviewUris] = useState( [] );
|
||||
@@ -46,12 +54,24 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
const [album, setAlbum] = useState( null );
|
||||
const [loading, setLoading] = useState( false );
|
||||
const [unsavedChanges, setUnsavedChanges] = useState( false );
|
||||
const [uploadProgress, setUploadProgress] = useState( { } );
|
||||
const [passesEvidenceTest, setPassesEvidenceTest] = useState( false );
|
||||
const [passesIdentificationTest, setPassesIdentificationTest] = useState( false );
|
||||
const [mediaViewerUris, setMediaViewerUris] = useState( [] );
|
||||
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState( 0 );
|
||||
const [groupedPhotos, setGroupedPhotos] = useState( [] );
|
||||
const [savingPhoto, setSavingPhoto] = useState( false );
|
||||
// state related to uploads
|
||||
const [uploadProgress, setUploadProgress] = useState( { } );
|
||||
const [uploadInProgress, setUploadInProgress] = useState( false );
|
||||
const [currentUploadIndex, setCurrentUploadIndex] = useState( 0 );
|
||||
const [error, setError] = useState( null );
|
||||
const [totalProgressIncrements, setTotalProgressIncrements] = useState( 0 );
|
||||
const [totalUploadProgress, setTotalUploadProgress] = useState( 0 );
|
||||
const [uploads, setUploads] = useState( [] );
|
||||
|
||||
const progress = totalProgressIncrements > 0
|
||||
? totalUploadProgress / totalProgressIncrements
|
||||
: 0;
|
||||
|
||||
const resetObsEditContext = useCallback( ( ) => {
|
||||
setObservations( [] );
|
||||
@@ -66,25 +86,33 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setGroupedPhotos( [] );
|
||||
}, [] );
|
||||
|
||||
useEffect( () => {
|
||||
const stopUpload = ( ) => {
|
||||
setUploadInProgress( false );
|
||||
setCurrentUploadIndex( 0 );
|
||||
setError( null );
|
||||
deactivateKeepAwake( );
|
||||
setTotalProgressIncrements( 0 );
|
||||
setTotalUploadProgress( 0 );
|
||||
setUploads( [] );
|
||||
};
|
||||
|
||||
useEffect( ( ) => {
|
||||
const currentProgress = uploadProgress;
|
||||
const progressListener = EventRegister.addEventListener(
|
||||
"INCREMENT_OBSERVATIONS_PROGRESS",
|
||||
INCREMENT_SINGLE_UPLOAD_PROGRESS,
|
||||
increments => {
|
||||
setUploadProgress( currentProgress => {
|
||||
increments.forEach( ( [uuid, increment] ) => {
|
||||
currentProgress[uuid] = currentProgress[uuid]
|
||||
? currentProgress[uuid]
|
||||
: 0;
|
||||
currentProgress[uuid] += increment;
|
||||
} );
|
||||
return { ...currentProgress };
|
||||
} );
|
||||
const uuid = increments[0];
|
||||
const increment = increments[1];
|
||||
|
||||
currentProgress[uuid] = ( uploadProgress[uuid] || 0 ) + increment;
|
||||
setTotalUploadProgress( totalUploadProgress + increment );
|
||||
setUploadProgress( currentProgress );
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
return ( ) => {
|
||||
EventRegister.removeEventListener( progressListener );
|
||||
};
|
||||
}, [] );
|
||||
}, [uploadProgress, totalUploadProgress] );
|
||||
|
||||
const allObsPhotoUris = useMemo(
|
||||
( ) => [...cameraPreviewUris, ...galleryUris],
|
||||
@@ -165,8 +193,10 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
}, [currentObservation] );
|
||||
|
||||
const addGalleryPhotosToCurrentObservation = useCallback( async photos => {
|
||||
setSavingPhoto( true );
|
||||
const obsPhotos = await createObsPhotos( photos );
|
||||
appendObsPhotos( obsPhotos );
|
||||
setSavingPhoto( false );
|
||||
}, [createObsPhotos, appendObsPhotos] );
|
||||
|
||||
const uploadValue = useMemo( ( ) => {
|
||||
@@ -217,6 +247,7 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
};
|
||||
|
||||
const addCameraPhotosToCurrentObservation = async localFilePaths => {
|
||||
setSavingPhoto( true );
|
||||
const obsPhotos = await Promise.all( localFilePaths.map(
|
||||
async photo => ObservationPhoto.new( photo )
|
||||
) );
|
||||
@@ -226,6 +257,7 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
localFilePaths
|
||||
);
|
||||
await savePhotosToCameraGallery( localFilePaths );
|
||||
setSavingPhoto( false );
|
||||
};
|
||||
|
||||
const updateObservationKeys = keysAndValues => {
|
||||
@@ -301,7 +333,83 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setLoading( false );
|
||||
};
|
||||
|
||||
const uploadObservation = async observation => {
|
||||
const markRecordUploaded = ( recordUUID, type, response ) => {
|
||||
if ( !response ) { return; }
|
||||
const { id } = response.results[0];
|
||||
|
||||
const record = realm.objectForPrimaryKey( type, recordUUID );
|
||||
realm?.write( ( ) => {
|
||||
record.id = id;
|
||||
record._synced_at = new Date( );
|
||||
} );
|
||||
};
|
||||
|
||||
const uploadToServer = async (
|
||||
evidenceUUID: string,
|
||||
type: string,
|
||||
params: Object,
|
||||
apiEndpoint: Function,
|
||||
options: Object,
|
||||
observationUUID?: string
|
||||
) => {
|
||||
emitUploadProgress( observationUUID, uploadProgressIncrement );
|
||||
const response = await createOrUpdateEvidence(
|
||||
apiEndpoint,
|
||||
params,
|
||||
options
|
||||
);
|
||||
if ( response ) {
|
||||
emitUploadProgress( observationUUID, uploadProgressIncrement );
|
||||
markRecordUploaded( evidenceUUID, type, response );
|
||||
}
|
||||
};
|
||||
|
||||
const uploadEvidence = async (
|
||||
evidence: Array<Object>,
|
||||
type: string,
|
||||
apiSchemaMapper: Function,
|
||||
observationId: ?number,
|
||||
apiEndpoint: Function,
|
||||
options: Object,
|
||||
observationUUID?: string,
|
||||
forceUpload?: boolean
|
||||
): Promise<any> => {
|
||||
// only try to upload evidence which is not yet on the server
|
||||
const unsyncedEvidence = forceUpload
|
||||
? evidence
|
||||
: evidence.filter( item => !item.wasSynced( ) );
|
||||
|
||||
const responses = await Promise.all( unsyncedEvidence.map( item => {
|
||||
const currentEvidence = item.toJSON( );
|
||||
const evidenceUUID = currentEvidence.uuid;
|
||||
|
||||
// Remove all null values, b/c the API doesn't seem to like them
|
||||
const newPhoto = {};
|
||||
const photo = currentEvidence?.photo;
|
||||
Object.keys( photo ).forEach( k => {
|
||||
if ( photo[k] !== null ) {
|
||||
newPhoto[k] = photo[k];
|
||||
}
|
||||
} );
|
||||
|
||||
currentEvidence.photo = newPhoto;
|
||||
|
||||
const params = apiSchemaMapper( observationId, currentEvidence );
|
||||
return uploadToServer(
|
||||
evidenceUUID,
|
||||
type,
|
||||
params,
|
||||
apiEndpoint,
|
||||
options,
|
||||
observationUUID
|
||||
);
|
||||
} ) );
|
||||
// eslint-disable-next-line consistent-return
|
||||
return responses[0];
|
||||
};
|
||||
|
||||
const uploadObservation = async obs => {
|
||||
setLoading( true );
|
||||
// don't bother trying to upload unless there's a logged in user
|
||||
if ( !currentUser ) { return {}; }
|
||||
if ( !apiToken ) {
|
||||
@@ -309,13 +417,83 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
"Gack, tried to upload an observation without API token!"
|
||||
);
|
||||
}
|
||||
activateKeepAwake();
|
||||
const response = Observation.uploadObservation(
|
||||
observation,
|
||||
apiToken,
|
||||
realm
|
||||
);
|
||||
deactivateKeepAwake();
|
||||
activateKeepAwake( );
|
||||
// every observation and observation photo counts for a total of 1 progress
|
||||
// we're showing progress in 0.5 increments: when an upload of obs/obsPhoto starts
|
||||
// and when the upload of obs/obsPhoto successfully completes
|
||||
emitUploadProgress( obs.uuid, uploadProgressIncrement );
|
||||
const obsToUpload = Observation.mapObservationForUpload( obs );
|
||||
const options = { api_token: apiToken };
|
||||
|
||||
// Remove all null values, b/c the API doesn't seem to like them for some
|
||||
// reason (might be an error with the API as of 20220801)
|
||||
const newObs = {};
|
||||
Object.keys( obsToUpload ).forEach( k => {
|
||||
if ( obsToUpload[k] !== null ) {
|
||||
newObs[k] = obsToUpload[k];
|
||||
}
|
||||
} );
|
||||
|
||||
let response;
|
||||
|
||||
// First upload the photos/sounds (before uploading the observation itself)
|
||||
const hasPhotos = obs?.observationPhotos?.length > 0;
|
||||
|
||||
await Promise.all( [
|
||||
hasPhotos
|
||||
? uploadEvidence(
|
||||
obs.observationPhotos,
|
||||
"ObservationPhoto",
|
||||
ObservationPhoto.mapPhotoForUpload,
|
||||
null,
|
||||
inatjs.photos.create,
|
||||
options
|
||||
)
|
||||
: null
|
||||
] );
|
||||
|
||||
const wasPreviouslySynced = obs.wasSynced( );
|
||||
const uploadParams = {
|
||||
observation: { ...newObs },
|
||||
fields: { id: true }
|
||||
};
|
||||
|
||||
if ( wasPreviouslySynced ) {
|
||||
response = await updateObservation( {
|
||||
...uploadParams,
|
||||
id: newObs.uuid,
|
||||
ignore_photos: true
|
||||
}, options );
|
||||
emitUploadProgress( obs.uuid, uploadProgressIncrement );
|
||||
} else {
|
||||
response = await createObservation( uploadParams, options );
|
||||
emitUploadProgress( obs.uuid, uploadProgressIncrement );
|
||||
}
|
||||
|
||||
if ( !response ) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { uuid: obsUUID } = response.results[0];
|
||||
|
||||
await Promise.all( [
|
||||
markRecordUploaded( obs.uuid, "Observation", response ),
|
||||
// Next, attach the uploaded photos/sounds to the uploaded observation
|
||||
hasPhotos
|
||||
? uploadEvidence(
|
||||
obs.observationPhotos,
|
||||
"ObservationPhoto",
|
||||
ObservationPhoto.mapPhotoForAttachingToObs,
|
||||
obsUUID,
|
||||
inatjs.observation_photos.create,
|
||||
options,
|
||||
obsUUID,
|
||||
true
|
||||
)
|
||||
: null
|
||||
] );
|
||||
deactivateKeepAwake( );
|
||||
setLoading( false );
|
||||
return response;
|
||||
};
|
||||
|
||||
@@ -368,31 +546,6 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
await Photo.deletePhoto( realm, photoUriToDelete );
|
||||
};
|
||||
|
||||
const startSingleUpload = async observation => {
|
||||
setLoading( true );
|
||||
const { uuid } = observation;
|
||||
setUploadProgress( {
|
||||
...uploadProgress,
|
||||
[uuid]: 0.5
|
||||
} );
|
||||
const response = await uploadObservation( observation );
|
||||
if ( Object.keys( response ).length === 0 ) {
|
||||
return;
|
||||
}
|
||||
// TODO: mostly making sure UI presentation works at the moment, but we will
|
||||
// need to figure out what counts as progress towards an observation uploading
|
||||
// and add that functionality.
|
||||
// maybe uploading an observation is 0.33, starting to upload photos is 0.5,
|
||||
// checking for sounds is 0.66 progress?
|
||||
// and we need a way to track this progress from the Observation.uploadObservation function
|
||||
|
||||
setLoading( false );
|
||||
setUploadProgress( {
|
||||
...uploadProgress,
|
||||
[uuid]: 1
|
||||
} );
|
||||
};
|
||||
|
||||
const downloadRemoteObservationsFromServer = async ( ) => {
|
||||
const params = {
|
||||
user_id: currentUser?.id,
|
||||
@@ -415,6 +568,38 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setLoading( false );
|
||||
};
|
||||
|
||||
const uploadMultipleObservations = ( ) => {
|
||||
if ( totalProgressIncrements === 0 ) {
|
||||
setTotalProgressIncrements( uploads.length + uploads
|
||||
.reduce( ( count, current ) => count
|
||||
+ current.observationPhotos.length, 0 ) );
|
||||
}
|
||||
const upload = async observationToUpload => {
|
||||
try {
|
||||
await uploadObservation( observationToUpload );
|
||||
} catch ( e ) {
|
||||
console.warn( e );
|
||||
setError( e.message );
|
||||
}
|
||||
setCurrentUploadIndex( currentIndex => currentIndex + 1 );
|
||||
};
|
||||
|
||||
const observationToUpload = uploads[currentUploadIndex];
|
||||
const continueUpload = observationToUpload && !!apiToken;
|
||||
|
||||
if ( !continueUpload ) {
|
||||
setUploadInProgress( false );
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadedUUIDS = Object.keys( uploadProgress );
|
||||
// only try to upload every observation once
|
||||
if ( !uploadedUUIDS.includes( observationToUpload.uuid ) ) {
|
||||
setUploadInProgress( true );
|
||||
upload( observationToUpload );
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createObservationNoEvidence,
|
||||
addObservations,
|
||||
@@ -450,7 +635,6 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setLoading,
|
||||
unsavedChanges,
|
||||
syncObservations,
|
||||
startSingleUpload,
|
||||
uploadProgress,
|
||||
setUploadProgress,
|
||||
saveAllObservations,
|
||||
@@ -464,8 +648,17 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
setSelectedPhotoIndex,
|
||||
groupedPhotos,
|
||||
setGroupedPhotos,
|
||||
stopUpload,
|
||||
uploadMultipleObservations,
|
||||
uploadInProgress,
|
||||
error,
|
||||
currentUploadIndex,
|
||||
progress,
|
||||
setUploads,
|
||||
uploads,
|
||||
originalCameraUrisMap,
|
||||
setOriginalCameraUrisMap
|
||||
setOriginalCameraUrisMap,
|
||||
savingPhoto
|
||||
};
|
||||
}, [
|
||||
currentObservation,
|
||||
@@ -495,10 +688,17 @@ const ObsEditProvider = ( { children }: Props ): Node => {
|
||||
mediaViewerUris,
|
||||
selectedPhotoIndex,
|
||||
groupedPhotos,
|
||||
currentUploadIndex,
|
||||
error,
|
||||
uploadInProgress,
|
||||
progress,
|
||||
totalProgressIncrements,
|
||||
uploads,
|
||||
setOriginalCameraUrisMap,
|
||||
originalCameraUrisMap,
|
||||
appendObsPhotos,
|
||||
cameraRollUris
|
||||
cameraRollUris,
|
||||
savingPhoto
|
||||
] );
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Realm } from "@realm/react";
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { createObservation, createOrUpdateEvidence, updateObservation } from "api/observations";
|
||||
import inatjs from "inaturalistjs";
|
||||
import { EventRegister } from "react-native-event-listeners";
|
||||
import uuid from "react-native-uuid";
|
||||
import { createObservedOnStringForUpload } from "sharedHelpers/dateAndTime";
|
||||
|
||||
@@ -246,231 +242,6 @@ class Observation extends Realm.Object {
|
||||
return unsyncedObs.length > 0;
|
||||
};
|
||||
|
||||
static markRecordUploaded = async ( recordUUID, type, response, realm ) => {
|
||||
const { id } = response.results[0];
|
||||
|
||||
const record = realm.objectForPrimaryKey( type, recordUUID );
|
||||
realm?.write( ( ) => {
|
||||
record.id = id;
|
||||
record._synced_at = new Date( );
|
||||
} );
|
||||
};
|
||||
|
||||
static uploadToServer = async (
|
||||
evidenceUUID: string,
|
||||
type: string,
|
||||
params: Object,
|
||||
apiEndpoint: Function,
|
||||
realm: any,
|
||||
options: Object
|
||||
) => {
|
||||
const response = await createOrUpdateEvidence( apiEndpoint, params, options );
|
||||
if ( response ) {
|
||||
await Observation.markRecordUploaded( evidenceUUID, type, response, realm );
|
||||
}
|
||||
};
|
||||
|
||||
static uploadEvidence = async (
|
||||
evidence: Array<Object>,
|
||||
type: string,
|
||||
apiSchemaMapper: Function,
|
||||
observationId: number,
|
||||
apiEndpoint: Function,
|
||||
realm: any,
|
||||
options: Object,
|
||||
forceUpload: boolean
|
||||
): Promise<any> => {
|
||||
// only try to upload evidence which is not yet on the server
|
||||
const unsyncedEvidence = forceUpload
|
||||
? evidence
|
||||
: evidence.filter( item => !item.wasSynced( ) );
|
||||
|
||||
const responses = await Promise.all( unsyncedEvidence.map( item => {
|
||||
const currentEvidence = item.toJSON( );
|
||||
const evidenceUUID = currentEvidence.uuid;
|
||||
|
||||
// Remove all null values, b/c the API doesn't seem to like them
|
||||
const newPhoto = {};
|
||||
const photo = currentEvidence?.photo;
|
||||
Object.keys( photo ).forEach( k => {
|
||||
if ( photo[k] !== null ) {
|
||||
newPhoto[k] = photo[k];
|
||||
}
|
||||
} );
|
||||
|
||||
currentEvidence.photo = newPhoto;
|
||||
|
||||
const params = apiSchemaMapper( observationId, currentEvidence );
|
||||
return Observation.uploadToServer(
|
||||
evidenceUUID,
|
||||
type,
|
||||
params,
|
||||
apiEndpoint,
|
||||
realm,
|
||||
options
|
||||
);
|
||||
} ) );
|
||||
// eslint-disable-next-line consistent-return
|
||||
return responses[0];
|
||||
};
|
||||
|
||||
static uploadObservation = async ( obs, apiToken, realm ) => {
|
||||
try {
|
||||
EventRegister.emit(
|
||||
"INCREMENT_OBSERVATIONS_PROGRESS",
|
||||
[[obs.uuid, 0.05]]
|
||||
);
|
||||
const obsToUpload = Observation.mapObservationForUpload( obs );
|
||||
const options = { api_token: apiToken };
|
||||
|
||||
// Remove all null values, b/c the API doesn't seem to like them for some
|
||||
// reason (might be an error with the API as of 20220801)
|
||||
const newObs = {};
|
||||
Object.keys( obsToUpload ).forEach( k => {
|
||||
if ( obsToUpload[k] !== null ) {
|
||||
newObs[k] = obsToUpload[k];
|
||||
}
|
||||
} );
|
||||
|
||||
const uploadParams = {
|
||||
observation: { ...newObs },
|
||||
fields: { id: true }
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
// First upload the photos/sounds (before uploading the observation itself)
|
||||
const hasPhotos = obs?.observationPhotos?.length > 0;
|
||||
const hasSounds = obs?.observationSounds?.length > 0;
|
||||
|
||||
await Promise.all( [
|
||||
hasPhotos
|
||||
? Observation.uploadEvidence(
|
||||
obs.observationPhotos,
|
||||
"ObservationPhoto",
|
||||
ObservationPhoto.mapPhotoForUpload,
|
||||
null,
|
||||
inatjs.photos.create,
|
||||
realm,
|
||||
options
|
||||
).then( () => {
|
||||
EventRegister.emit( "INCREMENT_OBSERVATIONS_PROGRESS", [[
|
||||
obs.uuid,
|
||||
hasSounds
|
||||
? 0.125
|
||||
: 0.25
|
||||
]] );
|
||||
} )
|
||||
: null,
|
||||
hasSounds
|
||||
? Observation.uploadEvidence(
|
||||
obs.observationSounds,
|
||||
"ObservationSound",
|
||||
ObservationSound.mapSoundForUpload,
|
||||
null,
|
||||
inatjs.sounds.create,
|
||||
realm,
|
||||
options
|
||||
).then( () => {
|
||||
EventRegister.emit( "INCREMENT_OBSERVATIONS_PROGRESS", [[
|
||||
obs.uuid,
|
||||
hasPhotos
|
||||
? 0.125
|
||||
: 0.25
|
||||
]] );
|
||||
} )
|
||||
: null
|
||||
] );
|
||||
|
||||
if ( !hasPhotos && !hasSounds ) {
|
||||
EventRegister.emit( "INCREMENT_OBSERVATIONS_PROGRESS", [[
|
||||
obs.uuid,
|
||||
0.25
|
||||
]] );
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
const wasPreviouslySynced = obs.wasSynced( );
|
||||
|
||||
if ( wasPreviouslySynced ) {
|
||||
response = await updateObservation( {
|
||||
id: newObs.uuid,
|
||||
ignore_photos: true,
|
||||
observation: { ...newObs },
|
||||
fields: { id: true }
|
||||
}, options );
|
||||
} else {
|
||||
// TODO - before creating observation, POST /v2/photos or POST /v2/sounds
|
||||
response = await createObservation( uploadParams, options );
|
||||
}
|
||||
|
||||
EventRegister.emit( "INCREMENT_OBSERVATIONS_PROGRESS", [[
|
||||
obs.uuid,
|
||||
0.3
|
||||
]] );
|
||||
|
||||
const { uuid: obsUUID } = response.results[0];
|
||||
await Promise.all( [
|
||||
Observation.markRecordUploaded( obs.uuid, "Observation", response, realm ),
|
||||
// Next, attach the uploaded photos/sounds to the uploaded observation
|
||||
hasPhotos
|
||||
? Observation.uploadEvidence(
|
||||
obs.observationPhotos,
|
||||
"ObservationPhoto",
|
||||
ObservationPhoto.mapPhotoForAttachingToObs,
|
||||
obsUUID,
|
||||
inatjs.observation_photos.create,
|
||||
realm,
|
||||
options,
|
||||
true
|
||||
).then( () => {
|
||||
EventRegister.emit( "INCREMENT_OBSERVATIONS_PROGRESS", [[
|
||||
obs.uuid,
|
||||
hasSounds
|
||||
? 0.2
|
||||
: 0.4
|
||||
]] );
|
||||
} )
|
||||
: null,
|
||||
hasSounds
|
||||
? Observation.uploadEvidence(
|
||||
obs.observationSounds,
|
||||
"ObservationSound",
|
||||
ObservationSound.mapSoundForAttachingToObs,
|
||||
obsUUID,
|
||||
inatjs.observation_sounds.create,
|
||||
realm,
|
||||
options,
|
||||
true
|
||||
).then( () => {
|
||||
EventRegister.emit( "INCREMENT_OBSERVATIONS_PROGRESS", [[
|
||||
obs.uuid,
|
||||
hasPhotos
|
||||
? 0.2
|
||||
: 0.4
|
||||
]] );
|
||||
} )
|
||||
: null
|
||||
] );
|
||||
|
||||
if ( !hasPhotos && !hasSounds ) {
|
||||
EventRegister.emit( "INCREMENT_OBSERVATIONS_PROGRESS", [[
|
||||
obs.uuid,
|
||||
0.4
|
||||
]] );
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch ( error ) {
|
||||
EventRegister.emit( "INCREMENT_OBSERVATIONS_PROGRESS", [[
|
||||
obs.uuid,
|
||||
-1
|
||||
]] );
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
static schema = {
|
||||
name: "Observation",
|
||||
primaryKey: "uuid",
|
||||
|
||||
13
src/sharedHelpers/emitUploadProgress.js
Normal file
13
src/sharedHelpers/emitUploadProgress.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { EventRegister } from "react-native-event-listeners";
|
||||
|
||||
export const INCREMENT_SINGLE_UPLOAD_PROGRESS = "singleUploadProgress";
|
||||
|
||||
const emitUploadProgress = ( observationUUID, increment ) => {
|
||||
if ( !observationUUID ) { return; }
|
||||
EventRegister.emit(
|
||||
INCREMENT_SINGLE_UPLOAD_PROGRESS,
|
||||
[observationUUID, increment]
|
||||
);
|
||||
};
|
||||
|
||||
export default emitUploadProgress;
|
||||
@@ -16,6 +16,5 @@ export { default as useObservationUpdatesWhenFocused } from "./useObservationUpd
|
||||
export { default as useShare } from "./useShare";
|
||||
export { default as useTranslation } from "./useTranslation";
|
||||
export { default as useUnlockScreen } from "./useUnlockScreen";
|
||||
export { default as useUploadObservations } from "./useUploadObservations";
|
||||
export { default as useUserLocation } from "./useUserLocation";
|
||||
export { default as useUserMe } from "./useUserMe";
|
||||
|
||||
@@ -18,7 +18,8 @@ const useInfiniteScroll = ( ): Object => {
|
||||
const baseParams = {
|
||||
user_id: currentUser?.id,
|
||||
per_page: 50,
|
||||
fields: Observation.FIELDS
|
||||
fields: Observation.FIELDS,
|
||||
ttl: -1
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -16,7 +16,6 @@ const useLocalObservations = ( ): Object => {
|
||||
// when they have lost focus, which prevents other
|
||||
// views from rendering when they have focus.
|
||||
const stagedObservationList = useRef( [] );
|
||||
const stagedObsToUpload = useRef( [] );
|
||||
const [observationList, setObservationList] = useState( [] );
|
||||
const [allObsToUpload, setAllObsToUpload] = useState( [] );
|
||||
|
||||
@@ -29,23 +28,13 @@ const useLocalObservations = ( ): Object => {
|
||||
const obs = realm.objects( "Observation" );
|
||||
const localObservations = obs.sorted( "_created_at", true );
|
||||
localObservations.addListener( ( collection, _changes ) => {
|
||||
if ( localObservations.length === 0 ) { return; }
|
||||
// started hitting https://github.com/realm/realm-js/issues/4484 on
|
||||
// 2022-09-13 for no reason i can discern Note that if you
|
||||
// setObservationsList to collection, it is a Realm.Collection, not an
|
||||
// array, which doesn't seem to work. _.compact or Array.from will
|
||||
// create an array of Realm objects... which will probably require some
|
||||
// degree of pagination in the future
|
||||
// setObservationList( _.compact( collection ) );
|
||||
stagedObservationList.current = [...collection];
|
||||
|
||||
const unsyncedObs = Observation.filterUnsyncedObservations( realm );
|
||||
|
||||
stagedObsToUpload.current = Array.from( unsyncedObs );
|
||||
|
||||
if ( isFocused ) {
|
||||
setObservationList( stagedObservationList.current );
|
||||
setAllObsToUpload( stagedObsToUpload.current );
|
||||
setAllObsToUpload( Array.from( unsyncedObs ) );
|
||||
}
|
||||
} );
|
||||
// eslint-disable-next-line consistent-return
|
||||
@@ -58,7 +47,6 @@ const useLocalObservations = ( ): Object => {
|
||||
useEffect( ( ) => {
|
||||
if ( isFocused ) {
|
||||
setObservationList( stagedObservationList.current );
|
||||
setAllObsToUpload( stagedObsToUpload.current );
|
||||
}
|
||||
}, [isFocused] );
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
activateKeepAwake,
|
||||
deactivateKeepAwake
|
||||
} from "@sayem314/react-native-keep-awake";
|
||||
import { RealmContext } from "providers/contexts";
|
||||
import { useEffect, useState } from "react";
|
||||
import { EventRegister } from "react-native-event-listeners";
|
||||
import Observation from "realmModels/Observation";
|
||||
import useApiToken from "sharedHooks/useApiToken";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
const useUploadObservations = ( allObsToUpload: Array<Object> ): Object => {
|
||||
const [uploadInProgress, setUploadInProgress] = useState( false );
|
||||
const [shouldUpload, setShouldUpload] = useState( false );
|
||||
const [currentUploadIndex, setCurrentUploadIndex] = useState( 0 );
|
||||
const [progress, setProgress] = useState( 0 );
|
||||
const [error, setError] = useState( null );
|
||||
const realm = useRealm( );
|
||||
const apiToken = useApiToken( );
|
||||
const [totalUploadCount, setTotalUploadCount] = useState( 0 );
|
||||
|
||||
const cleanup = ( ) => {
|
||||
setUploadInProgress( false );
|
||||
setShouldUpload( false );
|
||||
setCurrentUploadIndex( 0 );
|
||||
setError( null );
|
||||
deactivateKeepAwake( );
|
||||
setProgress( 0 );
|
||||
setTotalUploadCount( 0 );
|
||||
};
|
||||
|
||||
useEffect( ( ) => {
|
||||
if ( shouldUpload ) {
|
||||
EventRegister.emit(
|
||||
"INCREMENT_OBSERVATIONS_PROGRESS",
|
||||
allObsToUpload.map( observation => [observation.uuid, 0] )
|
||||
);
|
||||
}
|
||||
}, [shouldUpload, allObsToUpload] );
|
||||
|
||||
useEffect( ( ) => {
|
||||
const upload = async observationToUpload => {
|
||||
const increment = ( 1 / allObsToUpload.length ) / 2;
|
||||
setProgress( currentProgress => currentProgress + increment );
|
||||
try {
|
||||
await Observation.uploadObservation(
|
||||
observationToUpload,
|
||||
apiToken,
|
||||
realm
|
||||
);
|
||||
} catch ( e ) {
|
||||
console.warn( e );
|
||||
setError( e.message );
|
||||
}
|
||||
setProgress( currentProgress => {
|
||||
if ( currentUploadIndex === allObsToUpload.length - 1 ) {
|
||||
return 1;
|
||||
}
|
||||
return currentProgress + increment;
|
||||
} );
|
||||
setCurrentUploadIndex( currentIndex => currentIndex + 1 );
|
||||
};
|
||||
|
||||
const observationToUpload = allObsToUpload[currentUploadIndex];
|
||||
const continueUpload = shouldUpload && observationToUpload && !!apiToken;
|
||||
|
||||
if ( !continueUpload ) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTotalUploadCount( allObsToUpload.length );
|
||||
activateKeepAwake( );
|
||||
setUploadInProgress( true );
|
||||
upload( observationToUpload );
|
||||
}, [
|
||||
allObsToUpload,
|
||||
apiToken,
|
||||
shouldUpload,
|
||||
currentUploadIndex,
|
||||
realm
|
||||
] );
|
||||
|
||||
// // Fake upload in progress
|
||||
// return {
|
||||
// uploadInProgress: true,
|
||||
// error: null,
|
||||
// progress: 0.5,
|
||||
// stopUpload: cleanup,
|
||||
// currentUploadIndex: 0,
|
||||
// totalUploadCount: 1,
|
||||
// startUpload: ( ) => setShouldUpload( true ),
|
||||
// allObsToUpload: [{}, {}, {}, {}]
|
||||
// };
|
||||
|
||||
// // Fake error state
|
||||
// return {
|
||||
// uploadInProgress: false,
|
||||
// error: "Something went terribly wrong",
|
||||
// progress: 0,
|
||||
// stopUpload: cleanup,
|
||||
// currentUploadIndex: 0,
|
||||
// totalUploadCount: 1,
|
||||
// startUpload: ( ) => setShouldUpload( true ),
|
||||
// allObsToUpload: [{},{},{},{}]
|
||||
// };
|
||||
|
||||
return {
|
||||
uploadInProgress,
|
||||
error,
|
||||
progress,
|
||||
stopUpload: cleanup,
|
||||
currentUploadIndex,
|
||||
totalUploadCount,
|
||||
startUpload: ( ) => setShouldUpload( true ),
|
||||
allObsToUpload
|
||||
};
|
||||
};
|
||||
|
||||
export default useUploadObservations;
|
||||
@@ -25,5 +25,7 @@ export default define( "LocalObservation", faker => ( {
|
||||
// is this the right way to test this?
|
||||
needsSync: jest.fn( ),
|
||||
wasSynced: jest.fn( ),
|
||||
observed_on_string: "2022-12-03T11:14:16"
|
||||
observed_on_string: "2022-12-03T11:14:16",
|
||||
// This is a Realm object method that we use to see if a record was deleted or not
|
||||
isValid: jest.fn( () => true )
|
||||
} ) );
|
||||
|
||||
Reference in New Issue
Block a user