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": {