Handle modified but remotely deleted obs in upload process (#1628)

* Map realm observations to smaller objects for displaying in MyObservations

* Handle obs remotely deleted in upload UI and process; closes #1579

* Multiple modified and remotely deleted observations handled in upload process

* Revert to using full Realm objects in FlashList
This commit is contained in:
Amanda Bullington
2024-05-31 18:16:23 -07:00
committed by GitHub
parent dacaec4bbd
commit 34f18dc6bc
6 changed files with 113 additions and 17 deletions

View File

@@ -3,8 +3,8 @@ import { INatApiError } from "api/error";
import { deleteRemoteObservation } from "api/observations";
import { RealmContext } from "providers/contexts";
import { useCallback, useEffect } from "react";
import Observation from "realmModels/Observation";
import { log } from "sharedHelpers/logger";
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import { useAuthenticatedMutation } from "sharedHooks";
import useStore from "stores/useStore";
@@ -37,15 +37,8 @@ const useDeleteObservations = ( canBeginDeletions, myObservationsDispatch ): Obj
&& !deletionsInProgress
&& !deletionsComplete;
const deleteLocalObservation = useCallback( ( ) => {
const realmObservation = realm?.objectForPrimaryKey( "Observation", uuid );
logger.info( "Local observation to delete: ", realmObservation?.uuid );
if ( realmObservation ) {
safeRealmWrite( realm, ( ) => {
realm?.delete( realmObservation );
}, `deleting local observation ${realmObservation.uuid} in useDeleteObservations` );
logger.info( "Local observation deleted" );
}
const deleteLocalObservation = useCallback( async ( ) => {
await Observation.deleteLocalObservation( realm, uuid );
return true;
}, [realm, uuid] );
@@ -176,7 +169,9 @@ const useDeleteObservations = ( canBeginDeletions, myObservationsDispatch ): Obj
};
}, [deletionsComplete, error, resetDeleteObservationsSlice] );
return null;
return {
deleteLocalObservation
};
};
export default useDeleteObservations;

View File

@@ -30,6 +30,9 @@ export default useUploadObservations = ( ): Object => {
const setCurrentUpload = useStore( state => state.setCurrentUpload );
const currentUpload = useStore( state => state.currentUpload );
const setNumUnuploadedObservations = useStore( state => state.setNumUnuploadedObservations );
const removeDeletedObsFromUploadQueue = useStore(
state => state.removeDeletedObsFromUploadQueue
);
// The existing abortController lets you abort...
const abortController = useStore( storeState => storeState.abortController );
@@ -69,6 +72,7 @@ export default useUploadObservations = ( ): Object => {
] );
const uploadObservationAndCatchError = useCallback( async observation => {
const { uuid } = observation;
setCurrentUpload( observation );
try {
await uploadObservation( observation, realm, { signal: newAbortController( ).signal } );
@@ -80,8 +84,18 @@ export default useUploadObservations = ( ): Object => {
completeUploads( );
}
} catch ( uploadError ) {
console.log( uploadError, "upload error" );
const message = handleUploadError( uploadError, t );
addUploadError( message, observation.uuid );
if ( message?.match( /That observation no longer exists./ ) ) {
// 20240531 amanda - it seems like we have to update the UI
// for the progress bar before actually deleting the observation
// locally, otherwise Realm will throw an error while trying
// to load the individual progress for a deleted observation
removeDeletedObsFromUploadQueue( uuid );
await Observation.deleteLocalObservation( realm, uuid );
} else {
addUploadError( message, uuid );
}
}
}, [
addUploadError,
@@ -89,6 +103,7 @@ export default useUploadObservations = ( ): Object => {
currentUpload,
newAbortController,
realm,
removeDeletedObsFromUploadQueue,
removeFromUploadQueue,
setCurrentUpload,
t,

View File

@@ -288,6 +288,36 @@ class Observation extends Realm.Object {
};
}
// static mapObservationForFlashList( obs ) {
// return {
// _created_at: obs._created_at,
// _deleted_at: obs._deleted_at,
// _synced_at: obs._synced_at,
// _updated_at: obs._updated_at,
// uuid: obs.uuid,
// comments: obs.comments,
// description: obs.description,
// geoprivacy: obs.geoprivacy,
// id: obs.id,
// identifications: obs.identifications,
// latitude: obs.latitude,
// longitude: obs.longitude,
// observationPhotos: obs.observationPhotos,
// observationSounds: obs.observationSounds,
// observed_on_string: obs.observed_on_string,
// obscured: obs.obscured,
// place_guess: obs.place_guess,
// positional_accuracy: obs.positional_accuracy,
// quality_grade: obs.quality_grade,
// taxon: obs.taxon,
// time_observed_at: obs.time_observed_at,
// comments_viewed: obs.comments_viewed,
// identifications_viewed: obs.identifications_viewed,
// privateLatitude: obs.privateLatitude,
// privateLongitude: obs.privateLongitude
// };
// }
static projectUri = obs => {
const photo = obs?.observation_photos?.[0];
if ( !photo ) { return null; }
@@ -384,6 +414,17 @@ class Observation extends Realm.Object {
return updatedObs;
};
static deleteLocalObservation = async ( realm, uuidToDelete ) => {
const observation = realm?.objectForPrimaryKey( "Observation", uuidToDelete );
if ( observation ) {
await safeRealmWrite( realm, ( ) => {
realm?.delete( observation );
}, `deleting local observation ${uuidToDelete} in deleteLocalObservation` );
return true;
}
return false;
};
static schema = {
name: "Observation",
primaryKey: "uuid",

View File

@@ -284,7 +284,8 @@ export function handleUploadError( uploadError: Error | INatApiError, t: Functio
if ( e.message?.errors ) {
return e.message.errors.flat( ).join( ", " );
}
return e.message;
// 410 error for observations previously deleted uses e.message?.error format
return e.message?.error || e.message;
} ).join( ", " );
} else if ( uploadError.message?.match( /Network request failed/ ) ) {
message = t( "Connection-problem-Please-try-again-later" );

View File

@@ -6,6 +6,7 @@ import {
useEffect, useRef,
useState
} from "react";
// import Observation from "realmModels/Observation";
const { useRealm } = RealmContext;
@@ -31,6 +32,12 @@ const useLocalObservations = ( ): Object => {
if ( isFocused ) {
setObservationList( stagedObservationList.current );
// 20240530 amanda - we only need about half of the keys in an Observation object to
// display to the user on MyObservations, so I think passing around smaller objects
// will improve render time here. if it causes problems, we can remove and pass around
// the full realm object
// const mappedObservations = stagedObservationList.current
// .map( o => Observation.mapObservationForFlashList( o ) );
}
} );
// eslint-disable-next-line consistent-return

View File

@@ -1,3 +1,4 @@
import _ from "lodash";
import { RealmObservation } from "realmModels/types.d.ts";
const DEFAULT_STATE = {
@@ -76,6 +77,12 @@ const calculateTotalToolbarIncrements = uploads => countMappedIncrements(
uploads.map( u => countTotalIncrements( u ) )
);
const setTotalToolbarProgress = ( totalToolbarIncrements, totalUploadProgress ) => (
totalToolbarIncrements > 0
? setCurrentToolbarIncrements( totalUploadProgress ) / totalToolbarIncrements
: 0
);
const createUploadObservationsSlice: StateCreator<UploadObservationsSlice> = set => ( {
...DEFAULT_STATE,
resetUploadObservationsSlice: ( ) => set( DEFAULT_STATE ),
@@ -121,9 +128,7 @@ const createUploadObservationsSlice: StateCreator<UploadObservationsSlice> = set
= observation.currentIncrements / observation.totalIncrements;
return ( {
totalUploadProgress,
totalToolbarProgress: totalToolbarIncrements > 0
? setCurrentToolbarIncrements( totalUploadProgress ) / totalToolbarIncrements
: 0
totalToolbarProgress: setTotalToolbarProgress( totalToolbarIncrements, totalUploadProgress )
} );
} ),
setUploadStatus: uploadStatus => set( ( ) => ( {
@@ -171,7 +176,39 @@ const createUploadObservationsSlice: StateCreator<UploadObservationsSlice> = set
} ),
setNumUnuploadedObservations: numUnuploadedObservations => set( ( ) => ( {
numUnuploadedObservations
} ) )
} ) ),
removeDeletedObsFromUploadQueue: uuid => set( state => {
const {
numObservationsInQueue,
numUploadsAttempted,
totalToolbarIncrements,
totalUploadProgress: existingTotalUploadProgress,
uploadQueue
} = state;
// Zustand does *not* make deep copies when making supposedly immutable
// state changes, so for nested objects like this, we need to create a
// new object explicitly.
// https://github.com/pmndrs/zustand/blob/main/docs/guides/immutable-state-and-merging.md#nested-objects
const totalUploadProgress = existingTotalUploadProgress
? [...existingTotalUploadProgress]
: [];
const observation = totalUploadProgress.find( o => o.uuid === uuid );
observation.totalProgress = observation.totalIncrements;
observation.currentIncrements = observation.totalIncrements;
// return the new queue without the uuid of the object already deleted remotely
const queueWithDeleted = _.remove( uploadQueue, uuidInQueue => uuidInQueue !== uuid );
return ( {
uploadQueue: queueWithDeleted,
currentUpload: null,
totalUploadProgress,
totalToolbarProgress: setTotalToolbarProgress( totalToolbarIncrements, totalUploadProgress ),
uploadStatus: numUploadsAttempted === numObservationsInQueue
? "complete"
: "uploadInProgress"
} );
} )
} );
export default createUploadObservationsSlice;