From 34f18dc6bcf3113ed37e325cdff204fb09fd63e0 Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Fri, 31 May 2024 18:16:23 -0700 Subject: [PATCH] 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 --- .../hooks/useDeleteObservations.ts | 17 +++---- .../hooks/useUploadObservations.ts | 17 ++++++- src/realmModels/Observation.js | 41 +++++++++++++++++ src/sharedHelpers/uploadObservation.js | 3 +- src/sharedHooks/useLocalObservations.js | 7 +++ src/stores/createUploadObservationsSlice.ts | 45 +++++++++++++++++-- 6 files changed, 113 insertions(+), 17 deletions(-) diff --git a/src/components/MyObservations/hooks/useDeleteObservations.ts b/src/components/MyObservations/hooks/useDeleteObservations.ts index 06788041a..02b08cb8a 100644 --- a/src/components/MyObservations/hooks/useDeleteObservations.ts +++ b/src/components/MyObservations/hooks/useDeleteObservations.ts @@ -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; diff --git a/src/components/MyObservations/hooks/useUploadObservations.ts b/src/components/MyObservations/hooks/useUploadObservations.ts index 5cffc6d60..1f47f925a 100644 --- a/src/components/MyObservations/hooks/useUploadObservations.ts +++ b/src/components/MyObservations/hooks/useUploadObservations.ts @@ -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, diff --git a/src/realmModels/Observation.js b/src/realmModels/Observation.js index 92901f2ae..ece47133e 100644 --- a/src/realmModels/Observation.js +++ b/src/realmModels/Observation.js @@ -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", diff --git a/src/sharedHelpers/uploadObservation.js b/src/sharedHelpers/uploadObservation.js index 26d0eec0a..a6de12895 100644 --- a/src/sharedHelpers/uploadObservation.js +++ b/src/sharedHelpers/uploadObservation.js @@ -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" ); diff --git a/src/sharedHooks/useLocalObservations.js b/src/sharedHooks/useLocalObservations.js index 5dc0cc94e..6953fa14b 100644 --- a/src/sharedHooks/useLocalObservations.js +++ b/src/sharedHooks/useLocalObservations.js @@ -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 diff --git a/src/stores/createUploadObservationsSlice.ts b/src/stores/createUploadObservationsSlice.ts index e18d1fc8b..89bce0183 100644 --- a/src/stores/createUploadObservationsSlice.ts +++ b/src/stores/createUploadObservationsSlice.ts @@ -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 = set => ( { ...DEFAULT_STATE, resetUploadObservationsSlice: ( ) => set( DEFAULT_STATE ), @@ -121,9 +128,7 @@ const createUploadObservationsSlice: StateCreator = 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 = 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;