From f160875212b1be2dab900cf0e6c07b338a8a364d Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:44:22 -0700 Subject: [PATCH 1/5] Simplify progress bar event emitters (#663) * Simplify code for emitting events to progress bar and progress circle; closes #630 * Add incremental steps * Update individual progress circle correctly when a user saves changes to existing obs * Move upload observation code into ObsEditProvider (for single uploads) * Refactor upload from toolbar code into ObsEditProvider * Move event names into constants and fix tests * Fix multiple uploads * Fix test * Fix test * Remove multiple upload event listener --- .../project.pbxproj | 1 - src/components/MyObservations/Header.js | 6 +- .../MyObservations/MyObservations.js | 8 +- .../MyObservations/MyObservationsContainer.js | 6 +- .../MyObservations/ObsUploadStatus.js | 18 +- .../MyObservations/ToolbarContainer.js | 52 +-- src/components/ObsEdit/Header.js | 1 - .../UploadStatus/UploadStatus.js | 8 +- src/providers/ObsEditProvider.js | 301 ++++++++++++++---- src/realmModels/Observation.js | 229 ------------- src/sharedHelpers/emitUploadProgress.js | 13 + src/sharedHooks/index.js | 1 - src/sharedHooks/useLocalObservations.js | 6 +- src/sharedHooks/useUploadObservations.js | 122 ------- 14 files changed, 315 insertions(+), 457 deletions(-) create mode 100644 src/sharedHelpers/emitUploadProgress.js delete mode 100644 src/sharedHooks/useUploadObservations.js diff --git a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj index 55c9336c8..406e38088 100644 --- a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj +++ b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj @@ -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 = ""; }; 00E356F21AD99517003FC87E /* iNaturalistReactNativeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iNaturalistReactNativeTests.m; sourceTree = ""; }; - 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 = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = iNaturalistReactNative/AppDelegate.mm; sourceTree = ""; }; diff --git a/src/components/MyObservations/Header.js b/src/components/MyObservations/Header.js index bdc985037..50eb65ff6 100644 --- a/src/components/MyObservations/Header.js +++ b/src/components/MyObservations/Header.js @@ -20,7 +20,7 @@ type Props = { currentUser: ?Object, numObservations: number, setHeightAboveToolbar: Function, - uploadStatus: Object, + allObsToUpload: Array, 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} /> )} diff --git a/src/components/MyObservations/MyObservations.js b/src/components/MyObservations/MyObservations.js index 7fa450837..d280bd4fb 100644 --- a/src/components/MyObservations/MyObservations.js +++ b/src/components/MyObservations/MyObservations.js @@ -26,7 +26,7 @@ type Props = { observations: Array, onEndReached: Function, toggleLayout: Function, - uploadStatus: Object, + allObsToUpload: Array, 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,7 +215,7 @@ const MyObservations = ( { currentUser={currentUser} numObservations={observations.length} setHeightAboveToolbar={setHeightAboveToolbar} - uploadStatus={uploadStatus} + allObsToUpload={allObsToUpload} setShowLoginSheet={setShowLoginSheet} /> { 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} diff --git a/src/components/MyObservations/ObsUploadStatus.js b/src/components/MyObservations/ObsUploadStatus.js index e3b019f3b..641ef55cf 100644 --- a/src/components/MyObservations/ObsUploadStatus.js +++ b/src/components/MyObservations/ObsUploadStatus.js @@ -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 = ( ); - const progress = uploadProgress?.[observation.uuid]; - if ( !observation.id || typeof progress === "number" ) { + if ( !observation.id || typeof currentProgress === "number" ) { + const progress = currentProgress / totalProgressIncrements; return ( { + 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} diff --git a/src/components/MyObservations/ToolbarContainer.js b/src/components/MyObservations/ToolbarContainer.js index 52ada22d9..03f12a499 100644 --- a/src/components/MyObservations/ToolbarContainer.js +++ b/src/components/MyObservations/ToolbarContainer.js @@ -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, 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 ( diff --git a/src/components/ObsEdit/Header.js b/src/components/ObsEdit/Header.js index 4b9831208..094e8c857 100644 --- a/src/components/ObsEdit/Header.js +++ b/src/components/ObsEdit/Header.js @@ -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 ) ) { diff --git a/src/components/SharedComponents/UploadStatus/UploadStatus.js b/src/components/SharedComponents/UploadStatus/UploadStatus.js index db211e23d..4f4e7e848 100644 --- a/src/components/SharedComponents/UploadStatus/UploadStatus.js +++ b/src/components/SharedComponents/UploadStatus/UploadStatus.js @@ -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] ); diff --git a/src/providers/ObsEditProvider.js b/src/providers/ObsEditProvider.js index eb267354a..8ec0e0a95 100644 --- a/src/providers/ObsEditProvider.js +++ b/src/providers/ObsEditProvider.js @@ -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,23 @@ 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( [] ); + // 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 +85,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], @@ -301,7 +328,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, + type: string, + apiSchemaMapper: Function, + observationId: ?number, + apiEndpoint: Function, + options: Object, + observationUUID?: string, + forceUpload?: boolean + ): Promise => { + // 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 +412,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 +541,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 +563,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 +630,6 @@ const ObsEditProvider = ( { children }: Props ): Node => { setLoading, unsavedChanges, syncObservations, - startSingleUpload, uploadProgress, setUploadProgress, saveAllObservations, @@ -464,6 +643,14 @@ const ObsEditProvider = ( { children }: Props ): Node => { setSelectedPhotoIndex, groupedPhotos, setGroupedPhotos, + stopUpload, + uploadMultipleObservations, + uploadInProgress, + error, + currentUploadIndex, + progress, + setUploads, + uploads, originalCameraUrisMap, setOriginalCameraUrisMap }; @@ -495,6 +682,12 @@ const ObsEditProvider = ( { children }: Props ): Node => { mediaViewerUris, selectedPhotoIndex, groupedPhotos, + currentUploadIndex, + error, + uploadInProgress, + progress, + totalProgressIncrements, + uploads, setOriginalCameraUrisMap, originalCameraUrisMap, appendObsPhotos, diff --git a/src/realmModels/Observation.js b/src/realmModels/Observation.js index 3ec68d80d..812a1c323 100644 --- a/src/realmModels/Observation.js +++ b/src/realmModels/Observation.js @@ -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"; @@ -239,231 +235,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, - type: string, - apiSchemaMapper: Function, - observationId: number, - apiEndpoint: Function, - realm: any, - options: Object, - forceUpload: boolean - ): Promise => { - // 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", diff --git a/src/sharedHelpers/emitUploadProgress.js b/src/sharedHelpers/emitUploadProgress.js new file mode 100644 index 000000000..3afa00113 --- /dev/null +++ b/src/sharedHelpers/emitUploadProgress.js @@ -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; diff --git a/src/sharedHooks/index.js b/src/sharedHooks/index.js index 4e81bcec3..2c68c19a3 100644 --- a/src/sharedHooks/index.js +++ b/src/sharedHooks/index.js @@ -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"; diff --git a/src/sharedHooks/useLocalObservations.js b/src/sharedHooks/useLocalObservations.js index c68e351b5..fb127f8aa 100644 --- a/src/sharedHooks/useLocalObservations.js +++ b/src/sharedHooks/useLocalObservations.js @@ -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( [] ); @@ -41,11 +40,9 @@ const useLocalObservations = ( ): Object => { 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 +55,6 @@ const useLocalObservations = ( ): Object => { useEffect( ( ) => { if ( isFocused ) { setObservationList( stagedObservationList.current ); - setAllObsToUpload( stagedObsToUpload.current ); } }, [isFocused] ); diff --git a/src/sharedHooks/useUploadObservations.js b/src/sharedHooks/useUploadObservations.js deleted file mode 100644 index 2ee9bbccc..000000000 --- a/src/sharedHooks/useUploadObservations.js +++ /dev/null @@ -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 => { - 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; From 9bf791fe4a37d9d59e089625c92281883f7b1f55 Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:28:44 -0700 Subject: [PATCH 2/5] Add defaultValue to TextInputSheet; closes #657 (#670) --- src/components/SharedComponents/Sheets/TextInputSheet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/SharedComponents/Sheets/TextInputSheet.js b/src/components/SharedComponents/Sheets/TextInputSheet.js index 92bc79df4..baabb3c6b 100644 --- a/src/components/SharedComponents/Sheets/TextInputSheet.js +++ b/src/components/SharedComponents/Sheets/TextInputSheet.js @@ -70,6 +70,7 @@ const TextInputSheet = ( { textAlignVertical: "top" }} autoFocus + defaultValue={input} /> Date: Thu, 22 Jun 2023 15:18:22 -0700 Subject: [PATCH 3/5] Add skeleton loading wheel to EvidenceList; closes #658 (#671) --- src/components/ObsEdit/EvidenceList.js | 21 +++++++++++++++++++-- src/providers/ObsEditProvider.js | 11 +++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/components/ObsEdit/EvidenceList.js b/src/components/ObsEdit/EvidenceList.js index 3d2ca12e3..3cc4b8502 100644 --- a/src/components/ObsEdit/EvidenceList.js +++ b/src/components/ObsEdit/EvidenceList.js @@ -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 ( + + + + + + + + ); + } + return ( diff --git a/src/providers/ObsEditProvider.js b/src/providers/ObsEditProvider.js index 8ec0e0a95..b6c3789e1 100644 --- a/src/providers/ObsEditProvider.js +++ b/src/providers/ObsEditProvider.js @@ -59,6 +59,7 @@ const ObsEditProvider = ( { children }: Props ): Node => { 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 ); @@ -192,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( ( ) => { @@ -244,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 ) ) ); @@ -253,6 +257,7 @@ const ObsEditProvider = ( { children }: Props ): Node => { localFilePaths ); await savePhotosToCameraGallery( localFilePaths ); + setSavingPhoto( false ); }; const updateObservationKeys = keysAndValues => { @@ -652,7 +657,8 @@ const ObsEditProvider = ( { children }: Props ): Node => { setUploads, uploads, originalCameraUrisMap, - setOriginalCameraUrisMap + setOriginalCameraUrisMap, + savingPhoto }; }, [ currentObservation, @@ -691,7 +697,8 @@ const ObsEditProvider = ( { children }: Props ): Node => { setOriginalCameraUrisMap, originalCameraUrisMap, appendObsPhotos, - cameraRollUris + cameraRollUris, + savingPhoto ] ); return ( From 43309af43c2bd0edb3715a156ab974c7305613ac Mon Sep 17 00:00:00 2001 From: Ken-ichi Date: Thu, 22 Jun 2023 16:15:51 -0700 Subject: [PATCH 4/5] Fixed obs deletion bug (#545) and allow edit when obs has no media (closes #662) (#666) Obs deletion bug mostly has to do with Realm objects hanging around in memory when they're no longer in the database. I spent a long time trying to write a test that could see the custom header button menu we add via react-navigation but never got it to work. --- .../MyObservations/MyObservations.js | 2 +- src/components/ObsDetails/ObsDetails.js | 25 +++++++++++-------- src/sharedHooks/useInfiniteScroll.js | 3 ++- src/sharedHooks/useLocalObservations.js | 8 ------ tests/factories/LocalObservation.js | 4 ++- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/components/MyObservations/MyObservations.js b/src/components/MyObservations/MyObservations.js index d280bd4fb..8f067f524 100644 --- a/src/components/MyObservations/MyObservations.js +++ b/src/components/MyObservations/MyObservations.js @@ -220,7 +220,7 @@ const MyObservations = ( { /> o.isValid() )} key={layout} estimatedItemSize={ layout === "grid" diff --git a/src/components/ObsDetails/ObsDetails.js b/src/components/ObsDetails/ObsDetails.js index d064ed825..3626307c8 100644 --- a/src/components/ObsDetails/ObsDetails.js +++ b/src/components/ObsDetails/ObsDetails.js @@ -25,7 +25,7 @@ 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, @@ -231,6 +231,18 @@ const ObsDetails = (): Node => { } }, [observation, comments] ); + const editButton = useMemo( ( ) => ( + + ), [navToObsEdit, t] ); + if ( !observation ) { return null; } @@ -351,15 +363,7 @@ const ObsDetails = (): Node => { necessary ~~~kueda */} {/* TODO: a11y props are not passed down into this 3.party */} - + { editButton } { accessible accessibilityLabel={t( "Observation-has-no-photos-and-no-sounds" )} > + { editButton } { const baseParams = { user_id: currentUser?.id, per_page: 50, - fields: Observation.FIELDS + fields: Observation.FIELDS, + ttl: -1 }; const { diff --git a/src/sharedHooks/useLocalObservations.js b/src/sharedHooks/useLocalObservations.js index fb127f8aa..83a0c6386 100644 --- a/src/sharedHooks/useLocalObservations.js +++ b/src/sharedHooks/useLocalObservations.js @@ -28,14 +28,6 @@ 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 ); diff --git a/tests/factories/LocalObservation.js b/tests/factories/LocalObservation.js index 87637fbbb..09010cb5a 100644 --- a/tests/factories/LocalObservation.js +++ b/tests/factories/LocalObservation.js @@ -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 ) } ) ); From 53521804f2c20da997b31e97366ca2b31807f3b7 Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Thu, 22 Jun 2023 16:27:50 -0700 Subject: [PATCH 5/5] Update package; closes #660 (#672) --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 13d4f622f..9f400cf4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index d6ea8bca6..dff23c123 100644 --- a/package.json +++ b/package.json @@ -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",