From 9eb8a438849d78dfc74296a5974a337183025ad8 Mon Sep 17 00:00:00 2001
From: Patrick Fic <>
Date: Tue, 21 Jun 2022 11:41:42 -0700
Subject: [PATCH] Improve cloudinary media upload experience.
---
App.js | 26 +-
.../local-upload-progress.component.jsx | 13 +-
.../screen-main/screen-main.component.jsx | 30 +--
.../upload-progress.component.jsx | 252 ++++++++++--------
package.json | 1 +
util/document-upload.utility.js | 66 +----
util/local-document-upload.utility.js | 34 ---
yarn.lock | 9 +
8 files changed, 196 insertions(+), 235 deletions(-)
diff --git a/App.js b/App.js
index e8240b5..3afb8f3 100644
--- a/App.js
+++ b/App.js
@@ -13,7 +13,7 @@ import "intl/locale-data/jsonp/en";
import "./translations/i18n";
import "expo-asset";
import Toast from "react-native-toast-message";
-
+import { SafeAreaProvider } from "react-native-safe-area-context";
Sentry.init({
dsn: "https://8d6c3de1940a4e4f8b81cf4d2150bdea@o492140.ingest.sentry.io/5558869",
enableInExpoDevelopment: true,
@@ -26,7 +26,7 @@ const theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
- primary: "dodgerblue",
+ primary: "#1890ff",
accent: "tomato",
},
};
@@ -38,16 +38,18 @@ export default class App extends React.Component {
render() {
return (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
);
}
}
diff --git a/components/local-upload-progress/local-upload-progress.component.jsx b/components/local-upload-progress/local-upload-progress.component.jsx
index 5e19a67..a599f0c 100644
--- a/components/local-upload-progress/local-upload-progress.component.jsx
+++ b/components/local-upload-progress/local-upload-progress.component.jsx
@@ -1,6 +1,6 @@
import * as MediaLibrary from "expo-media-library";
import React, { useEffect, useState } from "react";
-import { useTranslation } from "react-i18next";
+
import {
ActivityIndicator,
Alert,
@@ -18,16 +18,11 @@ import {
selectCurrentCameraJobId,
selectDeleteAfterUpload,
} from "../../redux/app/app.selectors";
-import {
- selectBodyshop,
- selectCurrentUser,
-} from "../../redux/user/user.selectors";
+
import { formatBytes } from "../../util/document-upload.utility";
import { handleLocalUpload } from "../../util/local-document-upload.utility";
const mapStateToProps = createStructuredSelector({
- currentUser: selectCurrentUser,
- bodyshop: selectBodyshop,
selectedCameraJobId: selectCurrentCameraJobId,
deleteAfterUpload: selectDeleteAfterUpload,
});
@@ -35,8 +30,6 @@ const mapStateToProps = createStructuredSelector({
export default connect(mapStateToProps, null)(UploadProgress);
export function UploadProgress({
- currentUser,
- bodyshop,
selectedCameraJobId,
deleteAfterUpload,
uploads,
@@ -49,8 +42,6 @@ export function UploadProgress({
speed: 0,
});
- const { t } = useTranslation();
-
useEffect(() => {
//Set the state of uploads to do.
if (uploads) {
diff --git a/components/screen-main/screen-main.component.jsx b/components/screen-main/screen-main.component.jsx
index afd0b3c..07975d2 100644
--- a/components/screen-main/screen-main.component.jsx
+++ b/components/screen-main/screen-main.component.jsx
@@ -169,25 +169,23 @@ export function ScreenMainComponent({
}, [checkUserSession]);
return (
-
-
- {currentUser.authorized === null ? (
-
- ) : currentUser.authorized ? (
- bodyshop ? (
- HasAccess(bodyshop) ? (
-
- ) : (
-
- )
+
+ {currentUser.authorized === null ? (
+
+ ) : currentUser.authorized ? (
+ bodyshop ? (
+ HasAccess(bodyshop) ? (
+
) : (
-
+
)
) : (
-
- )}
-
-
+
+ )
+ ) : (
+
+ )}
+
);
}
export default connect(
diff --git a/components/upload-progress/upload-progress.component.jsx b/components/upload-progress/upload-progress.component.jsx
index 4297c5f..23ff355 100644
--- a/components/upload-progress/upload-progress.component.jsx
+++ b/components/upload-progress/upload-progress.component.jsx
@@ -28,6 +28,8 @@ import {
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { formatBytes, handleUpload } from "../../util/document-upload.utility";
+import Toast from "react-native-toast-message";
+import { validateArgCount } from "@firebase/util";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -44,12 +46,17 @@ export function UploadProgress({
selectedCameraJobId,
deleteAfterUpload,
uploads,
+ setUploads,
forceRerender,
}) {
const [progress, setProgress] = useState({
- loading: false,
uploadInProgress: false,
- speed: 0,
+ totalToUpload: 0,
+ totalUploaded: 0,
+ startTime: null,
+ totalFiles: 0,
+ totalFilesCompleted: 0,
+ currentFile: null,
files: {}, //uri is the key, value is progress
});
@@ -59,65 +66,71 @@ export function UploadProgress({
const { t } = useTranslation();
useEffect(() => {
- //Set the state of uploads to do.
-
- if (uploads) onDone(uploads);
+ if (uploads) {
+ onDone(uploads);
+ setUploads(null);
+ }
}, [uploads]);
- //if (!uploads) return null;
-
- function handleOnSuccess(id, asset) {
- logImEXEvent("imexmobile_successful_upload");
+ function handleOnSuccess(asset) {
+ //NEEDS REDO.
filesToDelete.push(asset);
setProgress((progress) => ({
...progress,
- action: t("mediabrowser.labels.converting"),
+ // totalUploaded: progress.totalToUpload + asset.size,
+ totalFilesCompleted: progress.totalFilesCompleted + 1,
files: {
...progress.files,
- [id]: {
- ...progress.files[id],
- percent: 1,
- action: t("mediabrowser.labels.converting"),
- },
- },
- // });
- }));
- }
-
- function handleOnProgress(uri, percent, loaded) {
- setProgress((progress) => ({
- ...progress,
- speed: loaded - progress.files[uri].loaded,
- action:
- percent === 1
- ? t("mediabrowser.labels.converting")
- : t("mediabrowser.labels.uploading"),
- files: {
- ...progress.files,
- [uri]: {
- ...progress.files[uri],
- percent,
- speed: loaded - progress.files[uri].loaded,
- action:
- percent === 1
- ? t("mediabrowser.labels.converting")
- : t("mediabrowser.labels.uploading"),
- loaded: loaded,
+ [asset.uri]: {
+ ...progress.files[asset.uri],
+ uploadEnd: new Date(),
},
},
}));
}
- function handleOnError(...props) {
- logImEXEvent("imexmobile_upload_documents_error", { props });
- }
- const onDone = async (data) => {
- //Validate to make sure the totals for the file sizes do not exceed the total on the job.
- setProgress({
- files: _.keyBy(data, "id"),
- loading: true,
- uploadInProgress: true,
+ function handleOnProgress({ uri, filename }, percent, loaded) {
+ //NEED REDO
+ setProgress((progress) => {
+ return {
+ ...progress,
+ totalUploaded:
+ progress.totalUploaded +
+ (loaded - (progress.files[uri]?.loaded || 0)),
+ files: {
+ ...progress.files,
+ [uri]: {
+ ...progress.files[uri],
+ percent,
+ filename,
+ speed: loaded - (progress.files[uri]?.loaded || 0),
+ loaded: loaded,
+ uploadStart: progress.files[uri]?.uploadStart || new Date(),
+ },
+ },
+ };
});
+ }
+ function handleOnError(error) {
+ logImEXEvent("imexmobile_upload_documents_error", { error });
+ Toast.show({
+ type: "error",
+ text1: "Unable to upload documents.",
+ text2: error,
+ autoHide: false,
+ });
+ }
+
+ const onDone = async (selectedFiles) => {
+ //Validate to make sure the totals for the file sizes do not exceed the total on the job.
+ const data = [];
+ const totalOfUploads = await selectedFiles.reduce(async (acc, val) => {
+ //Get the size of the file based on URI.
+ const info = await FileSystem.getInfoAsync(val.uri, { size: true });
+ data.push({ ...info, ...val }); //Add in the size.
+ val.albumId && MediaLibrary.migrateAlbumIfNeededAsync(val.albumId);
+ return (await acc) + info.size;
+ }, 0);
if (selectedCameraJobId !== "temp") {
const queryData = await client.query({
@@ -127,13 +140,6 @@ export function UploadProgress({
jobId: selectedCameraJobId,
},
});
- const totalOfUploads = await data.reduce(async (acc, val) => {
- //Get the size of the file based on URI.
- const info = await FileSystem.getInfoAsync(val.uri, { size: true });
-
- val.albumId && MediaLibrary.migrateAlbumIfNeededAsync(val.albumId);
- return (await acc) + info.size;
- }, 0);
if (
bodyshop.jobsizelimit -
@@ -145,7 +151,7 @@ export function UploadProgress({
...progress,
speed: 0,
action: null,
- loading: false,
+
uploadInProgress: false,
}));
Alert.alert(
@@ -155,10 +161,26 @@ export function UploadProgress({
return;
}
}
+ //We made it this far. We have enough space, so let's start uploading.
- //Sequentially await the proms.
+ setProgress((progress) => {
+ return {
+ ...progress,
+ uploadInProgress: true,
+
+ totalToUpload: totalOfUploads,
+ totalUploaded: 0,
+ totalFilesCompleted: 0,
+ startTime: new Date(),
+ totalFiles: data.length,
+ currentFile: null,
+ files: {}, //uri is the key, value is progress
+ };
+ });
for (var i = 0; i < data.length + 4; i = i + 4) {
+ //Reset the files.
+ setProgress((progress) => ({ ...progress, files: {} }));
let proms = [];
if (data[i]) {
proms.push(CreateUploadProm(data[i]));
@@ -176,48 +198,51 @@ export function UploadProgress({
await Promise.all(proms);
}
+ //Everything is uploaded, delete the succesful ones.
if (deleteAfterUpload) {
try {
+ console.log("Trying to Delete", filesToDelete);
if (Platform.OS === "android") {
- const res = await Promise.all(
+ await Promise.all(
filesToDelete.map(async (f) =>
MediaLibrary.removeAssetsFromAlbumAsync(f, f.albumId)
)
);
}
-
- const deleteResult = await MediaLibrary.deleteAssetsAsync(
- filesToDelete
+ console.log(
+ "Delete Result",
+ await MediaLibrary.deleteAssetsAsync(filesToDelete.map((f) => f.id))
);
} catch (error) {
console.log("Unable to delete picture.", error);
}
}
filesToDelete = [];
- setProgress({
- loading: false,
- speed: 0,
- action: null,
+ //Reset state.
+
+ setProgress({
uploadInProgress: false,
- files: {}, //uri is the key, value is progress
+ totalToUpload: 0,
+ totalUploaded: 0,
+ totalFilesCompleted: 0,
+ startTime: null,
+ totalFiles: 0,
+ currentFile: null,
+ files: {},
});
forceRerender();
};
const CreateUploadProm = async (p) => {
- let filename;
- filename = p.filename || p.uri.split("/").pop();
-
- await handleUpload(
+ return handleUpload(
{
- filename,
mediaId: p.id,
onError: handleOnError,
onProgress: ({ percent, loaded }) =>
- handleOnProgress(p.id, percent, loaded),
- onSuccess: () => handleOnSuccess(p.id, p),
+ handleOnProgress(p, percent, loaded),
+ onSuccess: () => handleOnSuccess(p),
},
{
bodyshop: bodyshop,
@@ -226,20 +251,6 @@ export function UploadProgress({
photo: p,
}
);
-
- //Set the state to mark that it's done.
- setProgress((progress) => ({
- ...progress,
- action: null,
- speed: 0,
- files: {
- ...progress.files,
- [p.id]: {
- ...progress.files[p.id],
- action: null,
- },
- },
- }));
};
return (
@@ -248,15 +259,22 @@ export function UploadProgress({
animationType="slide"
transparent={true}
onRequestClose={() => {
- Alert.alert("Modal has been closed.");
+ Alert.alert("Cancel?", "Do you want to abort the upload?", [
+ {
+ text: "Yes",
+ onPress: () => {
+ setUploads(null);
+ setProgress(null);
+ },
+ },
+ { text: "No" },
+ ]);
}}
>
-
- {progress.loading && }
-
-
+
+
{Object.keys(progress.files).map((key) => (
-
+
{progress.files[key].filename}
@@ -266,24 +284,49 @@ export function UploadProgress({
style={styles.progress}
color={progress.files[key].percent === 1 ? "green" : "blue"}
/>
- {progress.files[key].speed !== 0 &&
- progress.files[key].speed &&
- !isNaN(progress.files[key].speed) ? (
- {`${formatBytes(progress.files[key].speed)}/sec`}
- ) : null}
+
+ {`${formatBytes(
+ progress.files[key].loaded /
+ (((progress.files[key].uploadEnd || new Date()) -
+ progress.files[key].uploadStart) /
+ 1000)
+ )}/sec`}
+ {progress.files[key].percent === 1 && (
+ <>
+
+ Processing...
+ >
+ )}
+
))}
-
+
+ {`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`}
+ {`${formatBytes(progress.totalUploaded)} of ${formatBytes(
+ progress.totalToUpload
+ )} uploaded.`}
+
+
);
}
const styles = StyleSheet.create({
- modal: {
+ modalContainer: {
+ display: "flex",
flex: 1,
- marginTop: 50,
- marginBottom: 60,
+ justifyContent: "center",
+ },
+ modal: {
+ //flex: 1,
+ display: "flex",
marginLeft: 20,
marginRight: 20,
backgroundColor: "white",
@@ -299,9 +342,8 @@ const styles = StyleSheet.create({
elevation: 5,
},
centeredView: {
- flex: 1,
- // justifyContent: "center",
- // alignItems: "center",
+ justifyContent: "center",
+ alignItems: "center",
marginTop: 22,
},
progressItem: {
diff --git a/package.json b/package.json
index aedeb20..d517bf3 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"expo-media-library": "~14.1.0",
"expo-permissions": "~13.2.0",
"expo-status-bar": "~1.3.0",
+ "expo-system-ui": "~1.2.0",
"expo-updates": "~0.13.2",
"expo-video-thumbnails": "~6.3.0",
"firebase": "^9.8.3",
diff --git a/util/document-upload.utility.js b/util/document-upload.utility.js
index 1eb7a19..9249cbe 100644
--- a/util/document-upload.utility.js
+++ b/util/document-upload.utility.js
@@ -13,13 +13,13 @@ var cleanAxios = axios.create();
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
export const handleUpload = async (ev, context) => {
- const { filename, mediaId, onError, onSuccess, onProgress } = ev;
+ const { mediaId, onError, onSuccess, onProgress } = ev;
const { bodyshop, jobId } = context;
const imageData = await MediaLibrary.getAssetInfoAsync(mediaId);
- const newFile = await (await fetch(imageData.localUri)).blob();
+ const newFile = await (await fetch(imageData.uri)).blob();
let extension = imageData.localUri.split(".").pop();
- let key = `${bodyshop.id}/${jobId}/${(filename || newFile.data.name).replace(
+ let key = `${bodyshop.id}/${jobId}/${newFile.data.name.replace(
/\.[^/.]+$/,
""
)}-${new Date().getTime()}`;
@@ -29,8 +29,8 @@ export const handleUpload = async (ev, context) => {
mediaId,
imageData,
extension,
- newFile.type,
- newFile,
+ newFile.type, //Filetype
+ newFile, //File
onError,
onSuccess,
onProgress,
@@ -51,30 +51,21 @@ export const uploadToCloudinary = async (
onProgress,
context
) => {
- const { bodyshop, jobId, billId, uploaded_by, callback, tagsArray, photo } =
- context;
+ const { bodyshop, jobId, uploaded_by } = context;
//Set variables for getting the signed URL.
let timestamp = Math.floor(Date.now() / 1000);
let public_id = key;
- let tags = `${bodyshop.textid},${
- tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
- }`;
- // let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
-
const upload_preset = fileType.startsWith("video")
? "incoming_upload_video"
: "incoming_upload";
//Get the signed url.
-
let signedURLResponse;
try {
signedURLResponse = await axios.post(`${env.API_URL}/media/sign`, {
public_id: public_id,
- tags: tags,
timestamp: timestamp,
-
upload_preset: upload_preset,
});
} catch (error) {
@@ -85,7 +76,6 @@ export const uploadToCloudinary = async (
if (signedURLResponse.status !== 200) {
console.log("Error Getting Signed URL", signedURLResponse.statusText);
if (onError) onError(signedURLResponse.statusText);
-
return { success: false, error: signedURLResponse.statusText };
}
@@ -112,7 +102,6 @@ export const uploadToCloudinary = async (
formData.append("upload_preset", upload_preset);
formData.append("api_key", env.REACT_APP_CLOUDINARY_API_KEY);
formData.append("public_id", public_id);
- formData.append("tags", tags);
formData.append("timestamp", timestamp);
formData.append("signature", signature);
@@ -124,13 +113,11 @@ export const uploadToCloudinary = async (
fileType
)}/upload`,
formData,
- {
- ...options,
- }
+ options
);
- // console.log("Cloudinary Upload Response", cloudinaryUploadResponse.data);
} catch (error) {
console.log("CLOUDINARY error", error.response, cloudinaryUploadResponse);
+ if (onError) onError(error.message);
return { success: false, error: error };
}
@@ -147,35 +134,10 @@ export const uploadToCloudinary = async (
//Insert the document with the matching key.
const documentInsert = await client.mutate({
mutation: INSERT_NEW_DOCUMENT,
-
- update: (cache, { data }) => {
- cache.modify({
- fields: {
- documents: (existingDocs = []) => {
- const newDocRef = cache.writeFragment({
- data: data.insert_documents.returning[0],
- fragment: gql`
- fragment newDoc on documents {
- id
- name
- key
- type
- takenat
- extension
- jobid
- }
- `,
- });
- return [...existingDocs, newDocRef];
- },
- },
- });
- },
variables: {
docInput: [
{
...(jobId ? { jobid: jobId } : {}),
- ...(billId ? { billid: billId } : {}),
uploaded_by: uploaded_by,
key: key,
type: fileType,
@@ -197,19 +159,8 @@ export const uploadToCloudinary = async (
status: "done",
key: documentInsert.data.insert_documents.returning[0].key,
});
- // notification["success"]({
- // message: i18n.t("documents.successes.insert"),
- // });
- if (callback) {
- callback();
- }
} else {
if (onError) onError(JSON.stringify(documentInsert.errors));
- // notification["error"]({
- // message: i18n.t("documents.errors.insert", {
- // message: JSON.stringify(JSON.stringify(documentInsert.errors)),
- // }),
- // });
return {
success: false,
error: JSON.stringify(documentInsert.errors),
@@ -234,6 +185,7 @@ export function formatBytes(a, b = 2) {
if (0 === a || !a) return "0 Bytes";
const c = 0 > b ? 0 : b,
d = Math.floor(Math.log(a) / Math.log(1024));
+
return (
parseFloat((a / Math.pow(1024, d)).toFixed(c)) +
" " +
diff --git a/util/local-document-upload.utility.js b/util/local-document-upload.utility.js
index 4502659..97e529b 100644
--- a/util/local-document-upload.utility.js
+++ b/util/local-document-upload.utility.js
@@ -52,40 +52,6 @@ export const handleLocalUpload = async ({
const formData = new FormData();
formData.append("jobid", jobid);
- // const imageData = await MediaLibrary.getAssetInfoAsync(mediaId);
- // const mimeType = mime.getType(imageData.uri);
-
- // //let thumb;
- // let fileData = {
- // uri: null,
- // type: null,
- // name: null,
- // };
- // if (mimeType === "image/heic") {
- // try {
- // thumb = await ImageManipulator.manipulateAsync(imageData.uri, [], {
- // format: "jpeg",
- // base64: true,
- // compress: 0.75,
- // });
- // const name = imageData.filename.split(".");
- // name.pop();
- // fileData = {
- // uri: thumb.uri,
- // type: "image/jpeg",
- // name: name.join("") + ".jpeg",
- // };
- // } catch (error) {
- // console.log(error);
- // onError && onError(error.message);
- // }
- // } else {
- // fileData = {
- // uri: imageData.localUri || imageData.uri,
- // type: mimeType,
- // name: filename,
- // };
- //}
const filesList = [];
for (const file of files) {
diff --git a/yarn.lock b/yarn.lock
index 5970eb6..7793f37 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4570,6 +4570,15 @@ expo-structured-headers@~2.2.0:
resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-2.2.1.tgz#739f969101de6bead921eee59e5899399ad67715"
integrity sha512-nY6GuvoS/U5XdhfBNmvXGRoGzIXywXpSZs2wdiP+FbS79P9UWyEqzgARrBTF+6pQxUVMs6/vdffxRpwhjwYPug==
+expo-system-ui@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/expo-system-ui/-/expo-system-ui-1.2.0.tgz#dd8a80f11420cfc65ae25b7ce6de6b60a7aa5b0e"
+ integrity sha512-jynjFNz38FeY/2u4EKvLJdIk0hZAhicd9lbjSRJUDTjhGhQmYDsCQ32NKp/X3DwBkugAP5nwDwa5S3eGk5i80Q==
+ dependencies:
+ "@expo/config-plugins" "^4.0.14"
+ "@react-native/normalize-color" "^2.0.0"
+ debug "^4.3.2"
+
expo-updates-interface@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-0.6.0.tgz#cef5250106e59572afdfcf245c534754c8c844ba"