From 31d151c3b48e5801ded872118dc12bcdc7517701 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 16 Oct 2025 14:29:10 -0700 Subject: [PATCH] Progress update cleanup and UI improvements. --- app/jobs/[jobId]/_layout.tsx | 1 + app/settings.tsx | 32 +--- .../upload-delete-switch.component.jsx | 5 +- components/error/error-display.jsx | 9 +- components/job-documents/job-documents.jsx | 2 + components/job-lines/job-lines.jsx | 3 +- components/job-notes/job-notes.jsx | 2 +- components/job-tombstone/job-tombstone.jsx | 3 +- components/jobs-list/job-list-item.jsx | 7 +- components/settings/settings.jsx | 56 ++++++ components/settings/upload-delete-switch.jsx | 53 ++++++ .../upload-progress/upload-progress.jsx | 92 +++++---- redux/photos/photos.actions.js | 22 ++- redux/photos/photos.reducer.js | 8 +- redux/photos/photos.sagas.js | 180 +++++++++++++++--- redux/photos/photos.types.js | 4 + translations/en-US/common.json | 3 +- 17 files changed, 373 insertions(+), 109 deletions(-) create mode 100644 components/settings/settings.jsx create mode 100644 components/settings/upload-delete-switch.jsx diff --git a/app/jobs/[jobId]/_layout.tsx b/app/jobs/[jobId]/_layout.tsx index f2f11f0..2745ec7 100644 --- a/app/jobs/[jobId]/_layout.tsx +++ b/app/jobs/[jobId]/_layout.tsx @@ -13,6 +13,7 @@ function JobTabLayout(props) { tabBarActiveTintColor: theme.colors.primary, tabBarPosition: "top", headerShown: false, + animation: "shift", tabBarStyle: { marginTop: -50, }, diff --git a/app/settings.tsx b/app/settings.tsx index 7714f2c..82f07b0 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -1,31 +1,5 @@ -import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component"; -import { selectBodyshop } from "@/redux/user/user.selectors"; -import { StyleSheet, Text, View } from "react-native"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; +import Settings from "../components/settings/settings"; -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, -}); - -export default connect(mapStateToProps, null)(Tab); - -function Tab({ bodyshop }) { - return ( - - Tab [Home|Settings] - - Using Local Media Server? {bodyshop?.uselocalmediaserver ? "Yes" : "No"} - - - - ); +export default function Tab() { + return ; } - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: "center", - alignItems: "center", - }, -}); diff --git a/components-old/upload-delete-switch/upload-delete-switch.component.jsx b/components-old/upload-delete-switch/upload-delete-switch.component.jsx index 02efb5d..eb89d22 100644 --- a/components-old/upload-delete-switch/upload-delete-switch.component.jsx +++ b/components-old/upload-delete-switch/upload-delete-switch.component.jsx @@ -1,7 +1,6 @@ -import React from "react"; import { useTranslation } from "react-i18next"; import { StyleSheet, Text, View } from "react-native"; -import { Checkbox, Switch } from "react-native-paper"; +import { Switch } from "react-native-paper"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { toggleDeleteAfterUpload } from "../../redux/app/app.actions"; @@ -19,7 +18,7 @@ export function UploadDeleteSwitch({ deleteAfterUpload, toggleDeleteAfterUpload, }) { - console.log("*** ~ deleteAfterUpload:", deleteAfterUpload); + const { t } = useTranslation(); return ( diff --git a/components/error/error-display.jsx b/components/error/error-display.jsx index 105af95..e48076b 100644 --- a/components/error/error-display.jsx +++ b/components/error/error-display.jsx @@ -1,8 +1,8 @@ import { useTranslation } from "react-i18next"; import { Text } from "react-native"; -import { Card } from "react-native-paper"; +import { Button, Card } from "react-native-paper"; -export default function ErrorDisplay({ errorMessage, error }) { +export default function ErrorDisplay({ errorMessage, error, onDismiss }) { const { t } = useTranslation(); return ( @@ -14,6 +14,11 @@ export default function ErrorDisplay({ errorMessage, error }) { error || "An unknown error has occured."} + {onDismiss ? ( + + + + ) : null} ); diff --git a/components/job-documents/job-documents.jsx b/components/job-documents/job-documents.jsx index d8566b4..200f1f9 100644 --- a/components/job-documents/job-documents.jsx +++ b/components/job-documents/job-documents.jsx @@ -11,6 +11,7 @@ import { View, } from "react-native"; import ImageView from "react-native-image-viewing"; +import { ActivityIndicator } from "react-native-paper"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import env from "../../env"; @@ -119,6 +120,7 @@ export function JobDocumentsComponent({ bodyshop }) { getPhotos(); }, [getPhotos]); + if (loading) return ; if (error) { return ; } diff --git a/components/job-lines/job-lines.jsx b/components/job-lines/job-lines.jsx index 7899086..bd24042 100644 --- a/components/job-lines/job-lines.jsx +++ b/components/job-lines/job-lines.jsx @@ -17,14 +17,13 @@ export default function JobLines() { skip: !jobId, }); - console.log("*** ~ JobLines ~ error:", error); const { t } = useTranslation(); const onRefresh = async () => { return refetch(); }; if (loading) { - return ; + return ; } if (error) { diff --git a/components/job-notes/job-notes.jsx b/components/job-notes/job-notes.jsx index eac52e5..85d1fd4 100644 --- a/components/job-notes/job-notes.jsx +++ b/components/job-notes/job-notes.jsx @@ -25,7 +25,7 @@ export default function JobNotes() { }; if (loading) { - return ; + return ; } if (error) { return ; diff --git a/components/job-tombstone/job-tombstone.jsx b/components/job-tombstone/job-tombstone.jsx index b2fff3d..226e0ce 100644 --- a/components/job-tombstone/job-tombstone.jsx +++ b/components/job-tombstone/job-tombstone.jsx @@ -23,14 +23,13 @@ export default function JobTombstone() { }); const theme = useTheme(); - console.log("*** ~ JobTombstone ~ theme:", theme.colors); const { t } = useTranslation(); const onRefresh = async () => { return refetch(); }; if (loading) { - return ; + return ; } if (!data.jobs_by_pk) { return ( diff --git a/components/jobs-list/job-list-item.jsx b/components/jobs-list/job-list-item.jsx index 46f4420..e58a0bd 100644 --- a/components/jobs-list/job-list-item.jsx +++ b/components/jobs-list/job-list-item.jsx @@ -133,7 +133,12 @@ const styles = StyleSheet.create({ borderWidth: StyleSheet.hairlineWidth, backdropFilter: "blur(20px)", // web only }, - cardContents: { flex: 1, flexDirection: "row", display: "flex" }, + cardContents: { + flex: 1, + flexDirection: "row", + display: "flex", + alignItems: "center", + }, headerRow: { flexDirection: "row", alignItems: "flex-start", diff --git a/components/settings/settings.jsx b/components/settings/settings.jsx new file mode 100644 index 0000000..6c97dd8 --- /dev/null +++ b/components/settings/settings.jsx @@ -0,0 +1,56 @@ +import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component"; +import { toggleDeleteAfterUpload } from "@/redux/app/app.actions"; +import { selectDeleteAfterUpload } from "@/redux/app/app.selectors"; +import { selectBodyshop } from "@/redux/user/user.selectors"; +import { formatBytes } from "@/util/uploadUtils"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { StyleSheet, View } from "react-native"; +import { Button, Divider, Text } from "react-native-paper"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import UploadDeleteSwitch from "./upload-delete-switch"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + deleteAfterUpload: selectDeleteAfterUpload, +}); + +const mapDispatchToProps = (dispatch) => ({ + toggleDeleteAfterUpload: () => dispatch(toggleDeleteAfterUpload()), +}); +export default connect(mapStateToProps, mapDispatchToProps)(Tab); + +function Tab({ bodyshop, deleteAfterUpload, toggleDeleteAfterUpload }) { + return ( + + Settings + + Media Storage:{" "} + {bodyshop?.uselocalmediaserver + ? bodyshop.localmediaserverhttp + : "Cloud"} + + {!bodyshop?.uselocalmediaserver && ( + Job Size Limit: {formatBytes(bodyshop?.jobsizelimit)} + )} + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, +}); diff --git a/components/settings/upload-delete-switch.jsx b/components/settings/upload-delete-switch.jsx new file mode 100644 index 0000000..7e9f263 --- /dev/null +++ b/components/settings/upload-delete-switch.jsx @@ -0,0 +1,53 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, Text, View } from "react-native"; +import { Switch } from "react-native-paper"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { toggleDeleteAfterUpload } from "../../redux/app/app.actions"; +import { selectDeleteAfterUpload } from "../../redux/app/app.selectors"; + +const mapStateToProps = createStructuredSelector({ + deleteAfterUpload: selectDeleteAfterUpload, +}); + +const mapDispatchToProps = (dispatch) => ({ + toggleDeleteAfterUpload: () => dispatch(toggleDeleteAfterUpload()), +}); + +export function UploadDeleteSwitch({ + deleteAfterUpload, + toggleDeleteAfterUpload, +}) { + + const { t } = useTranslation(); + return ( + + + {t("mediabrowser.labels.deleteafterupload")} + + { + toggleDeleteAfterUpload(); + }} + value={deleteAfterUpload} + /> + + ); +} +const styles = StyleSheet.create({ + container: { + display: "flex", + flexDirection: "row", + alignItems: "center", + margin: 10, + }, + + text: { + flex: 1, + }, +}); +export default connect(mapStateToProps, mapDispatchToProps)(UploadDeleteSwitch); diff --git a/components/upload-progress/upload-progress.jsx b/components/upload-progress/upload-progress.jsx index d5b57e3..65f21d7 100644 --- a/components/upload-progress/upload-progress.jsx +++ b/components/upload-progress/upload-progress.jsx @@ -1,28 +1,53 @@ +import { clearUploadError } from "@/redux/photos/photos.actions"; +import theme from "@/util/theme"; import { formatBytes } from "@/util/uploadUtils"; -import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; -import { ProgressBar } from "react-native-paper"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, View } from "react-native"; +import { ProgressBar, Text } from "react-native-paper"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { - selectPhotos, - selectUploadProgress, + selectPhotos, + selectUploadError, + selectUploadProgress, } from "../../redux/photos/photos.selectors"; +import ErrorDisplay from "../error/error-display"; const mapStateToProps = createStructuredSelector({ photos: selectPhotos, photoUploadProgress: selectUploadProgress, + uploadError: selectUploadError, +}); +const mapDispatchToProps = (dispatch) => ({ + clearError: () => dispatch(clearUploadError()), }); -export default connect(mapStateToProps, null)(UploadProgress); +export default connect(mapStateToProps, mapDispatchToProps)(UploadProgress); -export function UploadProgress({ photos, photoUploadProgress }) { +export function UploadProgress({ + photos, + photoUploadProgress, + uploadError, + clearError, +}) { + const { t } = useTranslation(); if (photos?.length === 0) return null; + if (uploadError) + return ; return ( + + {t("general.labels.uploadprogress")} + + {Object.keys(photoUploadProgress).map((key) => ( - + {photoUploadProgress[key].fileName} @@ -42,38 +67,14 @@ export function UploadProgress({ photos, photoUploadProgress }) { > {`${formatBytes( photoUploadProgress[key].loaded / - (((photoUploadProgress[key].uploadEnd || new Date()) - - photoUploadProgress[key].uploadStart) / + (((photoUploadProgress[key].endTime || new Date()) - + photoUploadProgress[key].startTime) / 1000) )}/sec`} - {photoUploadProgress[key].percent === 1 && ( - <> - - Processing... - - )} ))} - - { - // progress.statusText ? ( - // <> - // - // {progress.statusText} - // - // - // ) : ( - // <> - // {`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`} - // {`${formatBytes(progress.totalUploaded)} of ${formatBytes( - // progress.totalToUpload - // )} uploaded.`} - // - // ) - } - ); @@ -81,17 +82,19 @@ export function UploadProgress({ photos, photoUploadProgress }) { const styles = StyleSheet.create({ modalContainer: { display: "flex", - flex: 1, + // flex: 1, + marginTop: 14, + marginBottom: 14, justifyContent: "center", }, modal: { //flex: 1, display: "flex", - marginLeft: 20, - marginRight: 20, - backgroundColor: "white", + marginLeft: 12, + marginRight: 12, + backgroundColor: theme.colors.elevation.level3, borderRadius: 20, - padding: 18, + paddingTop: 12, shadowColor: "#000", shadowOffset: { width: 0, @@ -101,6 +104,13 @@ const styles = StyleSheet.create({ shadowRadius: 4, elevation: 5, }, + title: { + alignSelf: "center", + alignItems: "center", + marginBottom: 12, + paddingLeft: 12, + paddingRight: 12, + }, centeredView: { justifyContent: "center", alignItems: "center", @@ -116,6 +126,12 @@ const styles = StyleSheet.create({ }, progressText: { flex: 1, + flexShrink: 1, // allow shrinking so ellipsis can appear + minWidth: 0, // ensures proper shrinking inside a flex row (especially on web) + // (Optional) If you find web still not clipping, you can uncomment the next lines: + // overflow: 'hidden', + // textOverflow: 'ellipsis', + // whiteSpace: 'nowrap', }, progressBarContainer: { flex: 3, diff --git a/redux/photos/photos.actions.js b/redux/photos/photos.actions.js index 25ccdae..0995245 100644 --- a/redux/photos/photos.actions.js +++ b/redux/photos/photos.actions.js @@ -31,7 +31,25 @@ export const mediaUploadProgressBulk = (info) => ({ }); export const mediaUploadCompleted = (photo) => ({ - type: PhotosActionTypes.MEDIA_UPLOAD_COMPLETED - //payload: photo, + type: PhotosActionTypes.MEDIA_UPLOAD_COMPLETED, + payload: photo, +}); + +export const deleteMedia = (photos) => ({ + type: PhotosActionTypes.DELETE_MEDIA, + payload: photos, +}); + +export const deleteMediaSuccess = (photos) => ({ + type: PhotosActionTypes.DELETE_MEDIA_SUCCESS, + payload: photos, +}); + +export const deleteMediaFailure = (photos) => ({ + type: PhotosActionTypes.DELETE_MEDIA_FAILURE, + payload: photos, +}); +export const clearUploadError = () => ({ + type: PhotosActionTypes.CLEAR_UPLOAD_ERROR, }); diff --git a/redux/photos/photos.reducer.js b/redux/photos/photos.reducer.js index 42eafe0..f5aa471 100644 --- a/redux/photos/photos.reducer.js +++ b/redux/photos/photos.reducer.js @@ -33,7 +33,7 @@ const photosReducer = (state = INITIAL_STATE, action) => { case PhotosActionTypes.MEDIA_UPLOAD_SUCCESS_ONE: return { ...state, - progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], progress: 100, status: 'completed' } } + progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], progress: 100, status: 'completed', endTime: new Date() } } }; case PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_BULK: return { @@ -48,6 +48,12 @@ const photosReducer = (state = INITIAL_STATE, action) => { photos: [], progress: {} }; + case PhotosActionTypes.CLEAR_UPLOAD_ERROR: + return { + ...state, + photos: [], progress: {}, + uploadError: null, + }; default: return state; } diff --git a/redux/photos/photos.sagas.js b/redux/photos/photos.sagas.js index 7b6b593..559891c 100644 --- a/redux/photos/photos.sagas.js +++ b/redux/photos/photos.sagas.js @@ -1,15 +1,17 @@ import axios from "axios"; import Constants from "expo-constants"; import * as ImagePicker from "expo-image-picker"; +import * as MediaLibrary from "expo-media-library"; import moment from 'moment'; -import { all, call, delay, fork, put, select, takeEvery, takeLatest } from "redux-saga/effects"; +import { all, call, delay, put, select, takeEvery, takeLatest } from "redux-saga/effects"; import env from "../../env"; import { client } from '../../graphql/client'; -import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries"; +import { GET_DOC_SIZE_TOTALS, INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries"; import { axiosAuthInterceptorId } from "../../util/CleanAxios"; import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils'; import { selectBodyshop, selectCurrentUser } from "../user/user.selectors"; import { + deleteMediaSuccess, mediaUploadCompleted, mediaUploadFailure, mediaUploadProgressBulk, @@ -18,14 +20,40 @@ import { mediaUploadSuccessOne } from "./photos.actions"; +import i18n from "@/translations/i18n"; +import { Platform } from "react-native"; +import { selectDeleteAfterUpload } from "../app/app.selectors"; import PhotosActionTypes from "./photos.types"; +axios.interceptors.request.use( + function (config) { + config.metadata = { startTime: new Date() }; + return config; + }, + function (error) { + return Promise.reject(error); + } +); + +axios.interceptors.response.use( + function (response) { + response.config.metadata.endTime = new Date(); + response.duration = + response.config.metadata.endTime - response.config.metadata.startTime; + return response; + }, + function (error) { + error.config.metadata.endTime = new Date(); + error.duration = + error.config.metadata.endTime - error.config.metadata.startTime; + return Promise.reject(error); + } +); + //Required to prevent headers from getting set and rejected from Cloudinary. let cleanAxios = axios.create(); cleanAxios.interceptors.request.eject(axiosAuthInterceptorId); - - export function* onOpenImagePicker() { yield takeLatest(PhotosActionTypes.OPEN_IMAGE_PICKER, openImagePickerAction); } @@ -73,6 +101,17 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) { yield call(uploadToLocalMediaServer, photos, bodyshop, jobid); } else { + + //Check to see if the job has enough space before uploading. + + const hasEnoughSpace = yield call(checkJobSpace, jobid, photos, bodyshop); + if (!hasEnoughSpace) { + + alert(i18n.t("mediabrowser.labels.storageexceeded")); + yield put(mediaUploadFailure(i18n.t("mediabrowser.labels.storageexceeded"))); + return; + } + // Process photos in batches to avoid overwhelming the system const batchSize = 3; // Upload 3 photos concurrently const batches = []; @@ -83,7 +122,7 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) { // Process each batch sequentially, but photos within batch concurrently for (const batch of batches) { const uploadTasks = batch.map((photo, index) => - fork(uploadSinglePhoto, photo, bodyshop, index, jobid) + call(uploadSinglePhoto, photo, bodyshop, index, jobid) ); // Wait for current batch to complete before starting next batch yield all(uploadTasks); @@ -91,7 +130,8 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) { yield delay(100); } } - yield put(mediaUploadCompleted()); + console.log("All uploads completed. This shouldn't fire before the uploads are done."); + yield put(mediaUploadCompleted(photos)); } catch (error) { console.log("Saga Error: upload start", error, error.stack); @@ -99,6 +139,44 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) { } } +function* checkJobSpace(jobid, photos, bodyshop) { + try { + const totalOfUploads = photos.reduce((acc, val) => { + //Get the size of the file based on URI. + if (val.fileSize) { + return acc + val.fileSize; + } else { + alert("Asset is missing filesize. Cannot verify job space."); + return acc + } + }, 0); + + if (jobid !== "temp") { + const queryData = yield call(client.query, { + query: GET_DOC_SIZE_TOTALS, + fetchPolicy: "network-only", + variables: { + jobId: jobid, + }, + }); + + if ( + bodyshop.jobsizelimit - + queryData?.data?.documents_aggregate.aggregate.sum.size <= + totalOfUploads + ) { + //No more room... abandon ship. + return false; + } + } + return true; + } + catch (error) { + console.log("Error checking job space", error, error.stack); + return false; + } +} + function* uploadSinglePhoto(photo, bodyshop, index, jobid) { try { yield put(mediaUploadProgressOne({ ...photo, status: 'starting', progress: 0 })); @@ -110,9 +188,7 @@ function* uploadSinglePhoto(photo, bodyshop, index, jobid) { photoBlob.data.name ).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}` yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid); - yield put(mediaUploadSuccessOne(photo)); - } catch (error) { console.log(`Upload failed for photo ${photo.uri}:`, error); yield put(mediaUploadFailure({ ...photo, status: "error", error: error.message })); @@ -127,7 +203,7 @@ function* uploadToLocalMediaServer(photos, bodyshop, jobid) { ims_token: bodyshop.localmediatoken, }, onUploadProgress: (e) => { - put(mediaUploadProgressBulk({ progress: e.loaded / e.total, loaded: e.loaded })); + put(mediaUploadProgressBulk({ progress: e.loaded / e.total, loaded: e.loaded, total: e.total })); }, }; @@ -150,31 +226,22 @@ function* uploadToLocalMediaServer(photos, bodyshop, jobid) { formData, options ); - if (imexMediaServerResponse.status !== 200) { console.log("Error uploading documents:", JSON.stringify(imexMediaServerResponse, null, 2)); - } else { - - // onSuccess({ - // duration: imexMediaServerResponse.headers["x-response-time"], - // }); + console.log("Local media server upload complete:", imexMediaServerResponse.data); } } catch (error) { - console.log("Error uploading documents:", error.message, JSON.stringify(error, null, 2)); - } } catch (error) { console.log("Uncaught error", error); - - } } function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) { try { - yield put(mediaUploadProgressOne({ ...photo, status: 'uploading', })); + yield put(mediaUploadProgressOne({ ...photo, startTime: new Date(), status: 'uploading', })); //Get the signed url allowing us to PUT to S3. const signedURLResponse = yield call(axios.post, `${env.API_URL}/media/imgproxy/sign`, @@ -184,7 +251,6 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) jobid, } ); - if (signedURLResponse.status !== 200) { console.log("Error Getting Signed URL", signedURLResponse.statusText); throw new Error(`Error getting signed URL : ${signedURLResponse.statusText}`); @@ -228,10 +294,17 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) //Create doc record. const uploaded_by = yield select(selectCurrentUser); - const [date, time] = photo.exif?.DateTime?.split(' ') || []; - const [year, month, day] = date ? date.split(':') : []; - const [hours, minutes, seconds] = time ? time.split(':') : []; - const pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`); + let pictureMoment = null; + try { + if (photo.exif) { + const [date, time] = photo.exif?.DateTime?.split(' ') || []; + const [year, month, day] = date ? date.split(':') : []; + const [hours, minutes, seconds] = time ? time.split(':') : []; + pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`); + } + } catch (exifError) { + console.log('Error parsing exif date. Unable to set created date.', exifError); + } yield call(client.mutate, ({ mutation: INSERT_NEW_DOCUMENT, @@ -245,13 +318,14 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) extension: extension, bodyshopid: bodyshop.id, size: photoBlob.size, - ...(photo.exif?.DateTime //TODO :Need to find how to do this. + ...(pictureMoment && pictureMoment.isValid() ? { takenat: pictureMoment } : {}), }, ], }, })); + console.log("Upload and record creation successful for", photo.uri); } } catch (error) { @@ -261,7 +335,7 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) } // Handle cancellation of uploads -export function* onCancelUpload() { +function* onCancelUpload() { yield takeEvery(PhotosActionTypes.CANCEL_UPLOAD, cancelUploadAction); } @@ -274,10 +348,62 @@ function* cancelUploadAction({ payload: photoId }) { // } } +function* onMediaUploadCompleted() { + yield takeLatest(PhotosActionTypes.MEDIA_UPLOAD_COMPLETED, mediaUploadCompletedAction); +} + +function* mediaUploadCompletedAction({ payload: photos }) { + //Check if this should be getting deleted + const deletedAfterUpload = yield select(selectDeleteAfterUpload); + if ( + !deletedAfterUpload + ) { + //Nothing to do here. + return; + } + + try { + // Handle the completion of media uploads + const filesToDelete = [...photos] + + if (Platform.OS === "android") { + //Create a new asset with the first file to delete. + // console.log('Trying new delete.'); + yield MediaLibrary.getPermissionsAsync(false); + + const album = yield call(MediaLibrary.createAlbumAsync, + "ImEX Mobile Deleted", + filesToDelete.pop(), + false + ); + //Move the rest. + if (filesToDelete.length > 0) { + const moveResult = yield call(MediaLibrary.addAssetsToAlbumAsync, + filesToDelete, + album, + false + ); + } + yield call(MediaLibrary.deleteAlbumsAsync, album); + } else { + yield call(MediaLibrary.deleteAssetsAsync, filesToDelete.map(f => f.assetId)); + } + + yield put(deleteMediaSuccess(photos)); + + } catch (error) { + console.log("Saga Error: upload start", error, error.stack); + yield put(mediaUploadFailure(error.message)); + } +} + + + export function* photosSagas() { yield all([ call(onOpenImagePicker), call(onMediaUploadStart), + call(onMediaUploadCompleted) //call(onCancelUpload) ]); } \ No newline at end of file diff --git a/redux/photos/photos.types.js b/redux/photos/photos.types.js index e20a15c..6355af5 100644 --- a/redux/photos/photos.types.js +++ b/redux/photos/photos.types.js @@ -6,5 +6,9 @@ const PhotosActionTypes = { MEDIA_UPLOAD_PROGRESS_UPDATE_ONE: "MEDIA_UPLOAD_PROGRESS_UPDATE_ONE", MEDIA_UPLOAD_PROGRESS_UPDATE_BULK: "MEDIA_UPLOAD_PROGRESS_UPDATE_BULK", MEDIA_UPLOAD_COMPLETED: "MEDIA_UPLOAD_COMPLETED", + DELETE_MEDIA: "DELETE_MEDIA", + DELETE_MEDIA_SUCCESS: "DELETE_MEDIA_SUCCESS", + DELETE_MEDIA_FAILURE: "DELETE_MEDIA_FAILURE", + CLEAR_UPLOAD_ERROR: "CLEAR_UPLOAD_ERROR", }; export default PhotosActionTypes; diff --git a/translations/en-US/common.json b/translations/en-US/common.json index 27d4918..1fb2f5a 100644 --- a/translations/en-US/common.json +++ b/translations/en-US/common.json @@ -15,7 +15,8 @@ }, "labels": { "na": "N/A", - "error": "Error" + "error": "Error", + "uploadprogress": "Upload Progress" } }, "jobdetail": {