import { useApolloClient } from "@apollo/client"; import * as FileSystem from "expo-file-system"; import * as MediaLibrary from "expo-media-library"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, Alert, Modal, Platform, StyleSheet, Text, View, } from "react-native"; import { Divider, ProgressBar } from "react-native-paper"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../firebase/firebase.analytics"; import { GET_DOC_SIZE_TOTALS } from "../../graphql/documents.queries"; import { selectCurrentCameraJobId, selectDeleteAfterUpload, } from "../../redux/app/app.selectors"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; import { formatBytes, handleUpload } from "../../util/document-upload.utility"; import Toast from "react-native-toast-message"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, bodyshop: selectBodyshop, selectedCameraJobId: selectCurrentCameraJobId, deleteAfterUpload: selectDeleteAfterUpload, }); export default connect(mapStateToProps, null)(UploadProgress); export function UploadProgress({ currentUser, bodyshop, selectedCameraJobId, deleteAfterUpload, uploads, setUploads, forceRerender, }) { const [progress, setProgress] = useState({ uploadInProgress: false, totalToUpload: 0, totalUploaded: 0, startTime: null, totalFiles: 0, totalFilesCompleted: 0, currentFile: null, files: {}, //uri is the key, value is progress }); let filesToDelete = []; const client = useApolloClient(); const { t } = useTranslation(); useEffect(() => { if (uploads) { onDone(uploads); setUploads(null); } }, [uploads]); function handleOnSuccess(asset) { //NEEDS REDO. filesToDelete.push(asset); setProgress((progress) => ({ ...progress, // totalUploaded: progress.totalToUpload + asset.size, totalFilesCompleted: progress.totalFilesCompleted + 1, files: { ...progress.files, [asset.uri]: { ...progress.files[asset.uri], uploadEnd: new Date(), }, }, })); } 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) => { setProgress((progress) => { return { ...progress, uploadInProgress: true, statusText: "Preparing upload...", }; }); //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({ query: GET_DOC_SIZE_TOTALS, fetchPolicy: "network-only", variables: { jobId: selectedCameraJobId, }, }); if ( bodyshop.jobsizelimit - queryData.data.documents_aggregate.aggregate.sum.size <= totalOfUploads ) { //No more room... abandon ship. setProgress((progress) => ({ ...progress, speed: 0, action: null, statusText: null, uploadInProgress: false, })); Alert.alert( t("mediabrowser.labels.storageexceeded_title"), t("mediabrowser.labels.storageexceeded") ); return; } } //We made it this far. We have enough space, so let's start uploading. setProgress((progress) => ({ ...progress, totalToUpload: totalOfUploads, totalUploaded: 0, totalFilesCompleted: 0, startTime: new Date(), totalFiles: data.length, currentFile: null, statusText: 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])); } if (data[i + 1]) { proms.push(CreateUploadProm(data[i + 1])); } if (data[i + 2]) { proms.push(CreateUploadProm(data[i + 2])); } if (data[i + 3]) { proms.push(CreateUploadProm(data[i + 3])); } await Promise.all(proms); } //Everything is uploaded, delete the succesful ones. if (deleteAfterUpload) { try { console.log("Trying to Delete", filesToDelete); if (Platform.OS === "android") { await Promise.all( filesToDelete.map(async (f) => MediaLibrary.removeAssetsFromAlbumAsync(f, f.albumId) ) ); } console.log( "Delete Result", await MediaLibrary.deleteAssetsAsync(filesToDelete.map((f) => f.id)) ); } catch (error) { console.log("Unable to delete picture.", error); } } filesToDelete = []; //Reset state. setProgress({ uploadInProgress: false, totalToUpload: 0, totalUploaded: 0, totalFilesCompleted: 0, startTime: null, totalFiles: 0, currentFile: null, files: {}, }); forceRerender(); }; const CreateUploadProm = async (p) => { return handleUpload( { mediaId: p.id, onError: handleOnError, onProgress: ({ percent, loaded }) => handleOnProgress(p, percent, loaded), onSuccess: () => handleOnSuccess(p), }, { bodyshop: bodyshop, jobId: selectedCameraJobId !== "temp" ? selectedCameraJobId : null, uploaded_by: currentUser.email, photo: p, } ); }; return ( { Alert.alert("Cancel?", "Do you want to abort the upload?", [ { text: "Yes", onPress: () => { setUploads(null); setProgress({ uploadInProgress: false, totalToUpload: 0, totalUploaded: 0, totalFilesCompleted: 0, startTime: null, totalFiles: 0, currentFile: null, files: {}, }); }, }, { text: "No" }, ]); }} > {Object.keys(progress.files).map((key) => ( {progress.files[key].filename} {`${formatBytes( progress.files[key].loaded / (((progress.files[key].uploadEnd || new Date()) - progress.files[key].uploadStart) / 1000) )}/sec`} {progress.files[key].percent === 1 && ( <> Processing... )} ))} {progress.statusText ? ( <> {progress.statusText} ) : ( <> {`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`} {`${formatBytes(progress.totalUploaded)} of ${formatBytes( progress.totalToUpload )} uploaded.`} )} ); } const styles = StyleSheet.create({ modalContainer: { display: "flex", flex: 1, justifyContent: "center", }, modal: { //flex: 1, display: "flex", marginLeft: 20, marginRight: 20, backgroundColor: "white", borderRadius: 20, padding: 18, shadowColor: "#000", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.25, shadowRadius: 4, elevation: 5, }, centeredView: { justifyContent: "center", alignItems: "center", marginTop: 22, }, progressItem: { display: "flex", flexDirection: "row", alignItems: "center", marginBottom: 12, marginLeft: 12, marginRight: 12, }, progressText: { flex: 1, }, progressBarContainer: { flex: 3, marginLeft: 12, marginRight: 12, }, });