|
|
|
@@ -1,18 +1,21 @@
|
|
|
|
import axios from "axios";
|
|
|
|
import axios from "axios";
|
|
|
|
import Constants from "expo-constants";
|
|
|
|
import Constants from "expo-constants";
|
|
|
|
import * as ImagePicker from "expo-image-picker";
|
|
|
|
import * as ImagePicker from "expo-image-picker";
|
|
|
|
import { END, eventChannel } from 'redux-saga';
|
|
|
|
import moment from 'moment';
|
|
|
|
import { all, call, delay, fork, put, select, take, takeEvery, takeLatest } from "redux-saga/effects";
|
|
|
|
import { all, call, delay, fork, put, select, takeEvery, takeLatest } from "redux-saga/effects";
|
|
|
|
import env from "../../env";
|
|
|
|
import env from "../../env";
|
|
|
|
|
|
|
|
import { client } from '../../graphql/client';
|
|
|
|
|
|
|
|
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
|
|
|
import { axiosAuthInterceptorId } from "../../util/CleanAxios";
|
|
|
|
import { axiosAuthInterceptorId } from "../../util/CleanAxios";
|
|
|
|
import { fetchArrayBufferFromUri, fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
|
|
|
|
import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
|
|
|
|
import { selectBodyshop } from "../user/user.selectors";
|
|
|
|
import { selectBodyshop, selectCurrentUser } from "../user/user.selectors";
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
mediaUploadComplete,
|
|
|
|
|
|
|
|
mediaUploadFailure,
|
|
|
|
mediaUploadFailure,
|
|
|
|
mediaUploadProgressOne,
|
|
|
|
mediaUploadProgressOne,
|
|
|
|
mediaUploadStart
|
|
|
|
mediaUploadStart,
|
|
|
|
|
|
|
|
mediaUploadSuccessOne
|
|
|
|
} from "./photos.actions";
|
|
|
|
} from "./photos.actions";
|
|
|
|
|
|
|
|
|
|
|
|
import PhotosActionTypes from "./photos.types";
|
|
|
|
import PhotosActionTypes from "./photos.types";
|
|
|
|
|
|
|
|
|
|
|
|
//Required to prevent headers from getting set and rejected from Cloudinary.
|
|
|
|
//Required to prevent headers from getting set and rejected from Cloudinary.
|
|
|
|
@@ -44,6 +47,7 @@ export function* openImagePickerAction({ payload: jobid }) {
|
|
|
|
aspect: [4, 3],
|
|
|
|
aspect: [4, 3],
|
|
|
|
quality: 1,
|
|
|
|
quality: 1,
|
|
|
|
allowsMultipleSelection: true,
|
|
|
|
allowsMultipleSelection: true,
|
|
|
|
|
|
|
|
exif: true,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
if (!(result.canceled)) {
|
|
|
|
if (!(result.canceled)) {
|
|
|
|
yield put(mediaUploadStart({ photos: result.assets, jobid }));
|
|
|
|
yield put(mediaUploadStart({ photos: result.assets, jobid }));
|
|
|
|
@@ -80,10 +84,10 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
|
|
|
// Small delay between batches to prevent overwhelming the server
|
|
|
|
// Small delay between batches to prevent overwhelming the server
|
|
|
|
yield delay(100);
|
|
|
|
yield delay(100);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
yield put(mediaUploadComplete());
|
|
|
|
//yield put(mediaUploadSuccess(photo));
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.log("Saga Error: upload start", error);
|
|
|
|
console.log("Saga Error: upload start", error, error.stack);
|
|
|
|
yield put(mediaUploadFailure(error.message));
|
|
|
|
yield put(mediaUploadFailure(error.message));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -93,8 +97,6 @@ function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
|
|
|
|
yield put(mediaUploadProgressOne({ ...photo, status: 'starting', progress: 0 }));
|
|
|
|
yield put(mediaUploadProgressOne({ ...photo, status: 'starting', progress: 0 }));
|
|
|
|
|
|
|
|
|
|
|
|
const photoBlob = yield fetchImageFromUri(photo.uri); //has a name, and type.
|
|
|
|
const photoBlob = yield fetchImageFromUri(photo.uri); //has a name, and type.
|
|
|
|
const photoArrayBuffer = yield fetchArrayBufferFromUri(photo.uri);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const extension = photoBlob.data?.name?.split('.').pop();
|
|
|
|
const extension = photoBlob.data?.name?.split('.').pop();
|
|
|
|
|
|
|
|
|
|
|
|
const key = `${bodyshop.id}/${jobid}/${replaceAccents(
|
|
|
|
const key = `${bodyshop.id}/${jobid}/${replaceAccents(
|
|
|
|
@@ -102,15 +104,15 @@ function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
|
|
|
|
).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`
|
|
|
|
).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`
|
|
|
|
|
|
|
|
|
|
|
|
if (bodyshop.uselocalmediaserver) {
|
|
|
|
if (bodyshop.uselocalmediaserver) {
|
|
|
|
yield call(uploadToLocalMediaServer, photo, photoBlob, photoArrayBuffer, key, bodyshop, jobid);
|
|
|
|
yield call(uploadToLocalMediaServer, photo, photoBlob, extension, key, bodyshop, jobid);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
yield call(uploadToImageProxy, photo, photoBlob, photoArrayBuffer, key, bodyshop, jobid);
|
|
|
|
yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// yield put(mediaUploadSuccess({ photoId, photo }));
|
|
|
|
yield put(mediaUploadSuccessOne(photo));
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
console.log(`Upload failed for photo ${photo.assetId}:`, error);
|
|
|
|
console.log(`Upload failed for photo ${photo.uri}:`, error);
|
|
|
|
yield put(mediaUploadFailure({ ...photo, status: "error", error: error.message }));
|
|
|
|
yield put(mediaUploadFailure({ ...photo, status: "error", error: error.message }));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -152,7 +154,7 @@ function* uploadToLocalMediaServer(photo, key) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function* uploadToImageProxy(photo, photoBlob, photoArrayBuffer, key, bodyshop, jobid) {
|
|
|
|
function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
yield put(mediaUploadProgressOne({ ...photo, status: 'uploading', }));
|
|
|
|
yield put(mediaUploadProgressOne({ ...photo, status: 'uploading', }));
|
|
|
|
//Get the signed url allowing us to PUT to S3.
|
|
|
|
//Get the signed url allowing us to PUT to S3.
|
|
|
|
@@ -172,31 +174,69 @@ function* uploadToImageProxy(photo, photoBlob, photoArrayBuffer, key, bodyshop,
|
|
|
|
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } =
|
|
|
|
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } =
|
|
|
|
signedURLResponse.data.signedUrls[0];
|
|
|
|
signedURLResponse.data.signedUrls[0];
|
|
|
|
|
|
|
|
|
|
|
|
// Create an eventChannel to bridge imperative upload progress events into the saga world
|
|
|
|
let uploadResult
|
|
|
|
const uploadChannel = yield call(createAxiosUploadChannel, {
|
|
|
|
|
|
|
|
url: preSignedUploadUrlToS3,
|
|
|
|
|
|
|
|
data: photoArrayBuffer,
|
|
|
|
|
|
|
|
method: 'put',
|
|
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
|
|
'Content-Type': photoBlob.type,
|
|
|
|
|
|
|
|
'Content-Length': photoBlob.size,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
while (true) {
|
|
|
|
uploadResult = yield new Promise((resolve, reject) => {
|
|
|
|
const { progress, error, success } = yield take(uploadChannel);
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
if (progress != null) {
|
|
|
|
xhr.open("PUT", preSignedUploadUrlToS3);
|
|
|
|
yield put(mediaUploadProgressOne({ ...photo, progress }));
|
|
|
|
xhr.setRequestHeader("Content-Type", photoBlob.type);
|
|
|
|
} else if (error) {
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
xhr.upload.onprogress = (e) => {
|
|
|
|
} else if (success) {
|
|
|
|
console.log("*** ~ awaitnewPromise ~ event:", e);
|
|
|
|
yield put(mediaUploadProgressOne({ ...photo, progress: 100, status: 'completed' }));
|
|
|
|
if (e.lengthComputable) {
|
|
|
|
break;
|
|
|
|
console.log(`Upload progress for ${photo.uri}:`, e.loaded / e.total);
|
|
|
|
}
|
|
|
|
put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total }));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
};
|
|
|
|
uploadChannel.close();
|
|
|
|
|
|
|
|
|
|
|
|
xhr.onload = () => {
|
|
|
|
|
|
|
|
if (xhr.status === 200) {
|
|
|
|
|
|
|
|
resolve(true);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
reject(new Error(`Upload failed: ${xhr.statusText}`));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
xhr.onerror = (req, event) => {
|
|
|
|
|
|
|
|
reject(new Error("Network error"));
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
xhr.send(photoBlob);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
console.log("Error uploading to S3", error.message, error.stack);
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (uploadResult) {
|
|
|
|
|
|
|
|
//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}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const documentInsert = yield call(client.mutate, ({
|
|
|
|
|
|
|
|
mutation: INSERT_NEW_DOCUMENT,
|
|
|
|
|
|
|
|
variables: {
|
|
|
|
|
|
|
|
docInput: [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
...(jobid ? { jobid: jobid } : {}),
|
|
|
|
|
|
|
|
uploaded_by: uploaded_by.email,
|
|
|
|
|
|
|
|
key: s3Key,
|
|
|
|
|
|
|
|
type: photoBlob.type,
|
|
|
|
|
|
|
|
extension: extension,
|
|
|
|
|
|
|
|
bodyshopid: bodyshop.id,
|
|
|
|
|
|
|
|
size: photoBlob.size,
|
|
|
|
|
|
|
|
...(photo.exif?.DateTime //TODO :Need to find how to do this.
|
|
|
|
|
|
|
|
? { takenat: pictureMoment }
|
|
|
|
|
|
|
|
: {}),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
console.log("*** ~ uploadToImageProxy ~ documentInsert:", JSON.stringify(documentInsert, null, 2));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
} catch (error) {
|
|
|
|
@@ -205,38 +245,6 @@ function* uploadToImageProxy(photo, photoBlob, photoArrayBuffer, key, bodyshop,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helper: turn axios upload progress into an eventChannel so we can yield put from saga
|
|
|
|
|
|
|
|
function createAxiosUploadChannel({ url, data, method = 'put', headers }) {
|
|
|
|
|
|
|
|
return eventChannel(emitter => {
|
|
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
|
|
cleanAxios({
|
|
|
|
|
|
|
|
method,
|
|
|
|
|
|
|
|
url,
|
|
|
|
|
|
|
|
data,
|
|
|
|
|
|
|
|
headers,
|
|
|
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
|
|
|
// NOTE: cannot yield here, so we emit events and handle them in saga loop
|
|
|
|
|
|
|
|
onUploadProgress: (e) => {
|
|
|
|
|
|
|
|
console.log("Upload progress event:", e);
|
|
|
|
|
|
|
|
if (e && e.total) {
|
|
|
|
|
|
|
|
emitter({ progress: e.loaded / e.total });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}).then(() => {
|
|
|
|
|
|
|
|
emitter({ success: true });
|
|
|
|
|
|
|
|
emitter(END);
|
|
|
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
|
|
|
emitter({ error: err });
|
|
|
|
|
|
|
|
emitter(END);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Unsubscribe / cancellation logic
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
|
|
controller.abort()
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Handle cancellation of uploads
|
|
|
|
// Handle cancellation of uploads
|
|
|
|
export function* onCancelUpload() {
|
|
|
|
export function* onCancelUpload() {
|
|
|
|
yield takeEvery(PhotosActionTypes.CANCEL_UPLOAD, cancelUploadAction);
|
|
|
|
yield takeEvery(PhotosActionTypes.CANCEL_UPLOAD, cancelUploadAction);
|
|
|
|
|