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 Toast from 'react-native-toast-message'; import { connect } from 'react-redux'; import { createStructuredSelector } from 'reselect'; import * as Sentry from '@sentry/react-native'; 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'; 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 document.', 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') { //Create a new asset with the first file to delete. // console.log('Trying new delete.'); await MediaLibrary.getPermissionsAsync(false); // try { // const albumremove = await MediaLibrary.removeAssetsFromAlbumAsync( // filesToDelete, // filesToDelete[0].albumId // ); // Toast.show({ // type: 'info', // text1: 'removeAssetsFromAlbumAsync', // text2: JSON.stringify(albumremove), // autoHide: false, // }); // } catch (error) { // console.log("🚀 ~ onDone ~ error:", error) // Toast.show({ // type: 'error', // text1: 'removeAssetsFromAlbumAsync', // text2: JSON.stringify(error), // autoHide: false, // }); // } // try { // const delres = await MediaLibrary.deleteAssetsAsync( // filesToDelete.map((f) => f.id) // ); // Toast.show({ // type: 'info', // text1: 'deleteAssetsAsync', // text2: JSON.stringify(delres), // autoHide: false, // }); // } catch (error) { // console.log("🚀 ~ onDone ~ error:", error) // Toast.show({ // type: 'error', // text1: 'deleteAssetsAsync', // text2: JSON.stringify(error), // autoHide: false, // }); // } // try { // const res = Promise.all( // filesToDelete.map((f) => // FileSystem.deleteAsync(f.uri, { idempotent: true }) // ) // ); // Toast.show({ // type: 'info', // text1: 'FileSystemDelete', // text2: JSON.stringify(res), // autoHide: false, // }); // } catch (error) { // console.log("🚀 ~ onDone ~ error:", error) // Toast.show({ // type: 'error', // text1: 'FileSystemDelete', // text2: JSON.stringify(error), // autoHide: false, // }); // } try { const album = await MediaLibrary.createAlbumAsync( 'ImEX Mobile Deleted', filesToDelete.pop(), false ); //Move the rest. if (filesToDelete.length > 0) { const moveResult = await MediaLibrary.addAssetsToAlbumAsync( filesToDelete, album, false ); } const deleteResult = await MediaLibrary.deleteAlbumsAsync(album); Toast.show({ type: 'success', text1: 'Pictures deleted succesfully.', text2: JSON.stringify(deleteResult), autoHide: false, }); } catch (error) { console.log("🚀 ~ onDone ~ error:", error) Toast.show({ type: 'error', text1: 'deleteAlbumsAsync', text2: JSON.stringify(error), autoHide: false, }); } //Delete the album. //This defaults to delete all assets in the album. } else { await MediaLibrary.deleteAssetsAsync(filesToDelete.map((f) => f.id)); } } catch (error) { console.log('Unable to delete picture.', error); Sentry.Native.captureException(error); } } filesToDelete = []; Toast.show({ type: 'success', text1: ` Upload completed.`, // // text2: duration, }); //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, }, });