diff --git a/app.json b/app.json
index 1a75de3..3a6b221 100644
--- a/app.json
+++ b/app.json
@@ -6,7 +6,7 @@
"scheme": "imex-mobile-scheme",
"userInterfaceStyle": "automatic",
"extra": {
- "expover": "1",
+ "expover": "8",
"eas": {
"projectId": "ffe01f3a-d507-4698-82cd-da1f1cad450b"
}
@@ -14,10 +14,7 @@
"runtimeVersion": "appVersion",
"orientation": "default",
"icon": "./assets/ImEXlogo192noa.png",
- "platforms": [
- "ios",
- "android"
- ],
+ "platforms": ["ios", "android"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.imex.imexmobile",
@@ -51,9 +48,7 @@
"fallbackToCacheTimeout": 0,
"url": "https://u.expo.dev/ffe01f3a-d507-4698-82cd-da1f1cad450b"
},
- "assetBundlePatterns": [
- "**/*"
- ],
+ "assetBundlePatterns": ["**/*"],
"web": {
"favicon": "./assets/ImEXlogo192noa.png",
"config": {
diff --git a/components/settings/settings.jsx b/components/settings/settings.jsx
index 7ff0264..ccbba77 100644
--- a/components/settings/settings.jsx
+++ b/components/settings/settings.jsx
@@ -7,6 +7,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Application from "expo-application";
import Constants from "expo-constants";
import * as Notifications from "expo-notifications";
+import * as Updates from "expo-updates";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Alert, ScrollView, StyleSheet, View } from "react-native";
@@ -182,6 +183,44 @@ function Tab({ bodyshop, currentUser, signOutStart }) {
number: `${Constants.expoConfig.version}(${Application.nativeBuildVersion} - ${Constants.expoConfig.extra.expover})`,
})}
+
);
diff --git a/components/upload-progress/upload-progress.jsx b/components/upload-progress/upload-progress.jsx
index 9cfc589..0b19beb 100644
--- a/components/upload-progress/upload-progress.jsx
+++ b/components/upload-progress/upload-progress.jsx
@@ -1,10 +1,17 @@
import { useTheme } from "@/hooks";
-import { clearUploadError } from "@/redux/photos/photos.actions";
+import { cancelUploads, clearUploadError } from "@/redux/photos/photos.actions";
import { formatBytes } from "@/util/uploadUtils";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, StyleSheet, View } from "react-native";
-import { Divider, Modal, Portal, ProgressBar, Text } from "react-native-paper";
+import {
+ Button,
+ Divider,
+ Modal,
+ Portal,
+ ProgressBar,
+ Text,
+} from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
@@ -20,6 +27,7 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
clearError: () => dispatch(clearUploadError()),
+ cancelUploads: () => dispatch(cancelUploads()),
});
export default connect(mapStateToProps, mapDispatchToProps)(UploadProgress);
@@ -29,6 +37,7 @@ export function UploadProgress({
photoUploadProgress,
uploadError,
clearError,
+ cancelUploads,
}) {
const { t } = useTranslation();
const theme = useTheme();
@@ -42,6 +51,10 @@ export function UploadProgress({
return completed / total;
}, [photoUploadProgress]);
+ const handleCancelUploads = () => {
+ cancelUploads();
+ };
+
return (
@@ -95,6 +108,9 @@ export function UploadProgress({
))}
+
diff --git a/redux/photos/photos.actions.js b/redux/photos/photos.actions.js
index 0995245..9d71e37 100644
--- a/redux/photos/photos.actions.js
+++ b/redux/photos/photos.actions.js
@@ -53,3 +53,17 @@ export const clearUploadError = () => ({
type: PhotosActionTypes.CLEAR_UPLOAD_ERROR,
});
+
+export const addUploadCancelTask = (payload) => ({
+ type: PhotosActionTypes.ADD_UPLOAD_CANCEL_TASK,
+ payload,
+});
+
+export const removeUploadCancelTask = (payload) => ({
+ type: PhotosActionTypes.REMOVE_UPLOAD_CANCEL_TASK,
+ payload,
+});
+
+export const cancelUploads = () => ({
+ type: PhotosActionTypes.CANCEL_UPLOADS,
+});
\ No newline at end of file
diff --git a/redux/photos/photos.reducer.js b/redux/photos/photos.reducer.js
index 7f434d2..52b91e3 100644
--- a/redux/photos/photos.reducer.js
+++ b/redux/photos/photos.reducer.js
@@ -5,7 +5,9 @@ const INITIAL_STATE = {
uploadInProgress: true,
uploadError: null,
jobid: null,
- progress: {}
+ progress: {},
+ cancelTasks: {},
+ cancelTriggered: false,
};
const photosReducer = (state = INITIAL_STATE, action) => {
@@ -17,7 +19,9 @@ const photosReducer = (state = INITIAL_STATE, action) => {
jobid: action.payload.jobid,
uploadInProgress: true,
uploadError: null,
- progress: action.payload.progress || {}
+ progress: action.payload.progress || {},
+ cancelTasks: {},
+ cancelTriggered: false,
};
case PhotosActionTypes.MEDIA_UPLOAD_FAILURE:
return {
@@ -31,9 +35,12 @@ const photosReducer = (state = INITIAL_STATE, action) => {
progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], ...action.payload } }
};
case PhotosActionTypes.MEDIA_UPLOAD_SUCCESS_ONE:
+ const { [action.payload.assetId]: _, ...remainingTasks } = state.cancelTasks;
+
return {
...state,
- progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], progress: 100, status: 'completed', endTime: new Date() } }
+ progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], progress: 100, status: 'completed', endTime: new Date() } },
+ cancelTasks: remainingTasks
};
case PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_BULK:
return {
@@ -46,13 +53,35 @@ const photosReducer = (state = INITIAL_STATE, action) => {
uploadInProgress: false,
uploadError: null,
photos: [],
- progress: {}
+ progress: {},
+ cancelTasks: {}
};
case PhotosActionTypes.CLEAR_UPLOAD_ERROR:
return {
...state,
- photos: [], progress: {},
+ photos: [],
+ progress: {},
uploadError: null,
+ cancelTasks: {},
+ };
+ case PhotosActionTypes.ADD_UPLOAD_CANCEL_TASK:
+ return {
+ ...state,
+ cancelTasks: {
+ ...state.cancelTasks,
+ [action.payload.assetId]: action.payload.cancelTask,
+ },
+ };
+ case PhotosActionTypes.REMOVE_UPLOAD_CANCEL_TASK:
+ const { [action.payload.assetId]: _2, ...remainingTasks2 } = state.cancelTasks; //2 added for scoped variable conflict.
+ return {
+ ...state,
+ cancelTasks: remainingTasks2,
+ };
+ case PhotosActionTypes.CANCEL_UPLOADS:
+ return {
+ ...state,
+ cancelTriggered: true,
};
default:
return state;
diff --git a/redux/photos/photos.sagas.js b/redux/photos/photos.sagas.js
index ae8c9de..1f45643 100644
--- a/redux/photos/photos.sagas.js
+++ b/redux/photos/photos.sagas.js
@@ -1,5 +1,6 @@
import axios from "axios";
import Constants from "expo-constants";
+import * as FileSystem from "expo-file-system/legacy";
import * as ImagePicker from "expo-image-picker";
import * as MediaLibrary from "expo-media-library";
import _ from 'lodash';
@@ -15,6 +16,7 @@ import { selectDeleteAfterUpload } from "../app/app.selectors";
import { store } from "../store";
import { selectBodyshop, selectCurrentUser } from "../user/user.selectors";
import {
+ addUploadCancelTask,
deleteMediaSuccess,
mediaUploadCompleted,
mediaUploadFailure,
@@ -84,7 +86,7 @@ export function* openImagePickerAction({ payload: jobid }) {
yield put(mediaUploadStart({ photos: result.assets, jobid, progress: _.keyBy(result.assets, 'assetId') }));
}
} catch (error) {
- // console.log("Saga Error: open Picker", error);
+ console.log("Saga Error: open Picker", error);
}
}
@@ -119,13 +121,17 @@ 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) =>
- call(uploadSinglePhoto, photo, bodyshop, index, jobid)
+ const isCancelTriggered = yield select((state) => state.photos.cancelTriggered
);
- // Wait for current batch to complete before starting next batch
- yield all(uploadTasks);
- // Small delay between batches to prevent overwhelming the server
- yield delay(100);
+ if (!isCancelTriggered) {
+ const uploadTasks = batch.map((photo, index) =>
+ call(uploadSinglePhoto, photo, bodyshop, index, jobid)
+ );
+ // Wait for current batch to complete before starting next batch
+ yield all(uploadTasks);
+ // Small delay between batches to prevent overwhelming the server
+ yield delay(100);
+ }
}
}
@@ -260,39 +266,53 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
let uploadResult
try {
- uploadResult = yield new Promise((resolve, reject) => {
- console.log("Starting XHR")
- const xhr = new XMLHttpRequest();
- xhr.upload.onprogress = (e) => {
- console.log("Upload Progress:", e.loaded, e.total);
- store.dispatch({ ...photo, progress: e.loaded / e.total, loaded: e.loaded });
- put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total, loaded: e.loaded }));
+ // const s3PutResponse = yield call(cleanAxios.put,
+ // preSignedUploadUrlToS3,
+ // photoBlob,
+ // {
+ // headers: {
+ // "Content-Type": photoBlob.type
+ // },
+ // onUploadProgress: (e) => {
+ // const progress = e.loaded / e.total;
+ // console.log("Event Progress", e)
+ // put(mediaUploadProgressOne({ ...photo, progress, loaded: e.loaded }));
+ // },
+ // }
+ // );
+
+ const task = FileSystem.createUploadTask(
+ preSignedUploadUrlToS3,
+ photo.uri,
+ {
+ //fieldName: FIELD_NAME_OF_THE_FILE_IN_REQUEST,
+ httpMethod: "PUT",
+ uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
+ mimeType: photoBlob.type,
+ headers: {},
+ //parameters: {...OTHER PARAMS IN REQUEST},
+ },
+ (progressData) => {
+ const sent = progressData.totalBytesSent;
+ const total = progressData.totalBytesExpectedToSend;
+ const progress = sent / total;
+ console.log(progress, sent)
+ store.dispatch(mediaUploadProgressOne({ ...photo, progress, loaded: sent }));
+ // onUpload(Number(progress.toFixed(2)) * 100);
+ },
+ );
+
+ yield put(addUploadCancelTask({ assetId: photo.assetId, cancelTask: task.cancelAsync }));
+ uploadResult = yield task.uploadAsync();
- };
- xhr.open("PUT", preSignedUploadUrlToS3);
- xhr.setRequestHeader("Content-Type", photoBlob.type);
- xhr.onload = () => {
- if (xhr.status === 200) {
- console.log("XHR Done. Resolve promise.")
- resolve(true);
- } else {
- reject(new Error(`Upload failed: ${xhr.statusText}`));
- }
- };
- xhr.onerror = (req, event) => {
- reject(new Error("Network error"));
- };
- console.log("Sending XHR")
- xhr.send(photoBlob);
- });
} catch (error) {
console.log("Error uploading to S3", error.message, error.stack);
throw error;
}
- if (uploadResult) {
+ if (uploadResult.status === 200) {
//Create doc record.
const uploaded_by = yield select(selectCurrentUser);
@@ -328,26 +348,34 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
},
}));
console.log("Upload and record creation successful for", photo.uri);
+ } else {
+ console.log("Error uploading to Cloud", uploadResult);
+ throw new Error(`Cloud upload failed: ${uploadResult}`);
}
} catch (error) {
- console.log("Error uploading to image proxy", JSON.stringify(error));
- throw new Error(`Image proxy upload failed: ${error.message}`);
+ console.log("Error uploading to Cloud", JSON.stringify(error));
+ throw new Error(`Cloud upload failed: ${error.message}`);
}
}
// Handle cancellation of uploads
function* onCancelUpload() {
- yield takeEvery(PhotosActionTypes.CANCEL_UPLOAD, cancelUploadAction);
+ yield takeEvery(PhotosActionTypes.CANCEL_UPLOADS, cancelUploadAction);
}
-function* cancelUploadAction({ payload: photoId }) {
- // const task = uploadTasks.get(photoId);
- // if (task) {
- // yield cancel(task);
- // uploadTasks.delete(photoId);
- // yield put(mediaUploadFailure({ photoId, error: 'Upload cancelled' }));
- // }
+function* cancelUploadAction() {
+ const cancelTasks = yield select((state) => state.photos.cancelTasks);
+ try {
+ const tasksToCancel = Object.values(cancelTasks);
+ for (const cancelTask of tasksToCancel) {
+ console.log("*** ~ cancelUploadAction ~ cancelTask:", cancelTask);
+ cancelTask();
+ }
+ yield put({ type: PhotosActionTypes.CLEAR_UPLOAD_ERROR });
+ } catch (error) {
+ console.log("Error cancelling upload", error);
+ }
}
function* onMediaUploadCompleted() {
@@ -414,7 +442,7 @@ export function* photosSagas() {
call(onOpenImagePicker),
call(onMediaUploadStart),
call(onMediaUploadCompleted),
- call(onMediaUploadFailure)
- //call(onCancelUpload)
+ call(onMediaUploadFailure),
+ call(onCancelUpload)
]);
}
\ No newline at end of file
diff --git a/redux/photos/photos.types.js b/redux/photos/photos.types.js
index 6355af5..1b936ed 100644
--- a/redux/photos/photos.types.js
+++ b/redux/photos/photos.types.js
@@ -10,5 +10,8 @@ const PhotosActionTypes = {
DELETE_MEDIA_SUCCESS: "DELETE_MEDIA_SUCCESS",
DELETE_MEDIA_FAILURE: "DELETE_MEDIA_FAILURE",
CLEAR_UPLOAD_ERROR: "CLEAR_UPLOAD_ERROR",
+ ADD_UPLOAD_CANCEL_TASK: "ADD_UPLOAD_CANCEL_TASK",
+ REMOVE_UPLOAD_CANCEL_TASK: "REMOVE_UPLOAD_CANCEL_TASK",
+ CANCEL_UPLOADS: "CANCEL_UPLOADS",
};
export default PhotosActionTypes;