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"