Improve upload progress.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import UploadProgress from "@/components/upload-progress/upload-progress";
|
||||
import { checkUserSession } from "@/redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "@/redux/user/user.selectors";
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
@@ -161,6 +162,7 @@ function AppContent({ currentUser, checkUserSession, bodyshop }: any) {
|
||||
if (currentUser.authorized) {
|
||||
return (
|
||||
<ThemedLayout>
|
||||
<UploadProgress />
|
||||
<AuthenticatedLayout />
|
||||
</ThemedLayout>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<babeledit_project version="1.2" be_version="2.7.1">
|
||||
<babeledit_project be_version="2.7.1" version="1.2">
|
||||
<!--
|
||||
|
||||
BabelEdit project file
|
||||
@@ -236,6 +236,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>upload</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>uploadprogress</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
//import ErrorDisplay from "../error-display/error-display.component";
|
||||
import UploadProgress from "../upload-progress/upload-progress";
|
||||
import JobListItem from "./job-list-item";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -58,7 +57,6 @@ export function JobListComponent({ bodyshop }) {
|
||||
>
|
||||
Jobs
|
||||
</Text>
|
||||
<UploadProgress />
|
||||
<FlatList
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useTheme } from "@/hooks";
|
||||
import { clearUploadError } from "@/redux/photos/photos.actions";
|
||||
import { formatBytes } from "@/util/uploadUtils";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { ProgressBar, Text } from "react-native-paper";
|
||||
import { ScrollView, StyleSheet, View } from "react-native";
|
||||
import { Divider, Modal, Portal, ProgressBar, Text } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
@@ -10,7 +12,6 @@ import {
|
||||
selectUploadError,
|
||||
selectUploadProgress,
|
||||
} from "../../redux/photos/photos.selectors";
|
||||
import ErrorDisplay from "../error/error-display";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
photos: selectPhotos,
|
||||
@@ -30,78 +31,94 @@ export function UploadProgress({
|
||||
clearError,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (photos?.length === 0) return null;
|
||||
if (uploadError)
|
||||
return <ErrorDisplay error={uploadError} onDismiss={clearError} />;
|
||||
return (
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={styles.modal}>
|
||||
<Text variant="titleLarge" style={styles.title}>
|
||||
{t("general.labels.uploadprogress")}
|
||||
</Text>
|
||||
const theme = useTheme();
|
||||
|
||||
{Object.keys(photoUploadProgress).map((key) => (
|
||||
<View key={key} style={styles.progressItem}>
|
||||
<Text
|
||||
style={styles.progressText}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{photoUploadProgress[key].fileName}
|
||||
</Text>
|
||||
<View style={styles.progressBarContainer}>
|
||||
<ProgressBar
|
||||
progress={photoUploadProgress[key].progress}
|
||||
style={styles.progress}
|
||||
color={
|
||||
photoUploadProgress[key].progress === 1 ? "green" : "blue"
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
const completion = useMemo(() => {
|
||||
const total = Object.keys(photoUploadProgress).length;
|
||||
if (total === 0) return 0;
|
||||
const completed = Object.values(photoUploadProgress).filter(
|
||||
(p) => p.progress === 100
|
||||
).length;
|
||||
return completed / total;
|
||||
}, [photoUploadProgress]);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={photos?.length > 0}
|
||||
style={styles.modalOuter} // add
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: theme.colors.elevation.level1 },
|
||||
]}
|
||||
>
|
||||
<ScrollView style={styles.modalFill}>
|
||||
<Text variant="titleLarge" style={styles.title}>
|
||||
{t("general.labels.upload")}
|
||||
</Text>
|
||||
<Text variant="labelLarge">
|
||||
{`${t("general.labels.uploadprogress")} ${Math.round(
|
||||
completion * 100
|
||||
)}%`}
|
||||
</Text>
|
||||
<ProgressBar
|
||||
progress={completion}
|
||||
style={styles.progress}
|
||||
color={completion === 1 ? "green" : "blue"}
|
||||
/>
|
||||
<Divider style={{ marginVertical: 12 }} />
|
||||
{Object.keys(photoUploadProgress).map((key) => (
|
||||
<View key={key} style={styles.progressItem}>
|
||||
<Text
|
||||
style={styles.progressText}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
<Text>{`${formatBytes(
|
||||
photoUploadProgress[key].loaded /
|
||||
(((photoUploadProgress[key].endTime || new Date()) -
|
||||
photoUploadProgress[key].startTime) /
|
||||
1000)
|
||||
)}/sec`}</Text>
|
||||
{photoUploadProgress[key].fileName}
|
||||
</Text>
|
||||
<View style={styles.progressBarContainer}>
|
||||
<ProgressBar
|
||||
progress={(photoUploadProgress[key].progress || 0) / 100}
|
||||
style={styles.progress}
|
||||
color={
|
||||
photoUploadProgress[key].progress === 100 ? "green" : "blue"
|
||||
}
|
||||
/>
|
||||
<View style={styles.speedRow}>
|
||||
<Text>{`${formatBytes(
|
||||
photoUploadProgress[key].loaded /
|
||||
(((photoUploadProgress[key].endTime || new Date()) -
|
||||
photoUploadProgress[key].startTime) /
|
||||
1000)
|
||||
)}/sec`}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
display: "flex",
|
||||
// flex: 1,
|
||||
marginTop: 14,
|
||||
marginBottom: 14,
|
||||
justifyContent: "center",
|
||||
modalOuter: {
|
||||
flex: 1, // ensure outer container can grow,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 72,
|
||||
},
|
||||
modal: {
|
||||
//flex: 1,
|
||||
display: "flex",
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
//backgroundColor: theme.colors.elevation.level3,
|
||||
borderRadius: 20,
|
||||
paddingTop: 12,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
modalContainer: {
|
||||
width: "100%",
|
||||
height: "50%", // force full area (important for iOS)
|
||||
padding: 24,
|
||||
justifyContent: "center",
|
||||
borderRadius: 24,
|
||||
},
|
||||
modalFill: {
|
||||
flex: 1,
|
||||
},
|
||||
speedRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
title: {
|
||||
alignSelf: "center",
|
||||
@@ -110,11 +127,6 @@ const styles = StyleSheet.create({
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
},
|
||||
centeredView: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginTop: 22,
|
||||
},
|
||||
progressItem: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -17,7 +17,7 @@ const photosReducer = (state = INITIAL_STATE, action) => {
|
||||
jobid: action.payload.jobid,
|
||||
uploadInProgress: true,
|
||||
uploadError: null,
|
||||
progress: {}
|
||||
progress: action.payload.progress || {}
|
||||
};
|
||||
case PhotosActionTypes.MEDIA_UPLOAD_FAILURE:
|
||||
return {
|
||||
|
||||
@@ -2,13 +2,17 @@ import axios from "axios";
|
||||
import Constants from "expo-constants";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import * as MediaLibrary from "expo-media-library";
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Alert, Platform } from "react-native";
|
||||
import { all, call, delay, put, select, takeEvery, takeLatest } from "redux-saga/effects";
|
||||
import env from "../../env";
|
||||
import { client } from '../../graphql/client';
|
||||
import { GET_DOC_SIZE_TOTALS, INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import { axiosAuthInterceptorId } from "../../util/CleanAxios";
|
||||
import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
|
||||
import { selectDeleteAfterUpload } from "../app/app.selectors";
|
||||
import { store } from "../store";
|
||||
import { selectBodyshop, selectCurrentUser } from "../user/user.selectors";
|
||||
import {
|
||||
deleteMediaSuccess,
|
||||
@@ -19,12 +23,9 @@ import {
|
||||
mediaUploadStart,
|
||||
mediaUploadSuccessOne
|
||||
} from "./photos.actions";
|
||||
|
||||
import i18n from "@/translations/i18n";
|
||||
import { Platform } from "react-native";
|
||||
import { selectDeleteAfterUpload } from "../app/app.selectors";
|
||||
import PhotosActionTypes from "./photos.types";
|
||||
|
||||
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
config.metadata = { startTime: new Date() };
|
||||
@@ -80,7 +81,7 @@ export function* openImagePickerAction({ payload: jobid }) {
|
||||
exif: true,
|
||||
});
|
||||
if (!(result.canceled)) {
|
||||
yield put(mediaUploadStart({ photos: result.assets, jobid }));
|
||||
yield put(mediaUploadStart({ photos: result.assets, jobid, progress: _.keyBy(result.assets, 'assetId') }));
|
||||
}
|
||||
} catch (error) {
|
||||
// console.log("Saga Error: open Picker", error);
|
||||
@@ -101,14 +102,13 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
||||
else {
|
||||
|
||||
//Check to see if the job has enough space before uploading.
|
||||
// const hasEnoughSpace = yield call(checkJobSpace, jobid, photos, bodyshop);
|
||||
// if (!hasEnoughSpace) {
|
||||
|
||||
const hasEnoughSpace = yield call(checkJobSpace, jobid, photos, bodyshop);
|
||||
if (!hasEnoughSpace) {
|
||||
|
||||
alert(i18n.t("mediabrowser.labels.storageexceeded"));
|
||||
yield put(mediaUploadFailure(i18n.t("mediabrowser.labels.storageexceeded")));
|
||||
return;
|
||||
}
|
||||
// alert(i18n.t("mediabrowser.labels.storageexceeded"));
|
||||
// yield put(mediaUploadFailure(i18n.t("mediabrowser.labels.storageexceeded")));
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Process photos in batches to avoid overwhelming the system
|
||||
const batchSize = 3; // Upload 3 photos concurrently
|
||||
@@ -137,43 +137,45 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
||||
}
|
||||
}
|
||||
|
||||
function* checkJobSpace(jobid, photos, bodyshop) {
|
||||
try {
|
||||
const totalOfUploads = photos.reduce((acc, val) => {
|
||||
//Get the size of the file based on URI.
|
||||
if (val.fileSize) {
|
||||
return acc + val.fileSize;
|
||||
} else {
|
||||
alert("Asset is missing filesize. Cannot verify job space.");
|
||||
return acc
|
||||
}
|
||||
}, 0);
|
||||
// function* checkJobSpace(jobid, photos, bodyshop) {
|
||||
// try {
|
||||
// //TODO - This function has not been validated and saw issues during testing.
|
||||
// //It was not fixed as we will not be enabling it.
|
||||
// const totalOfUploads = photos.reduce((acc, val) => {
|
||||
// //Get the size of the file based on URI.
|
||||
// if (val.fileSize) {
|
||||
// return acc + val.fileSize;
|
||||
// } else {
|
||||
// alert("Asset is missing filesize. Cannot verify job space.");
|
||||
// return acc
|
||||
// }
|
||||
// }, 0);
|
||||
|
||||
if (jobid !== "temp") {
|
||||
const queryData = yield call(client.query, {
|
||||
query: GET_DOC_SIZE_TOTALS,
|
||||
fetchPolicy: "network-only",
|
||||
variables: {
|
||||
jobId: jobid,
|
||||
},
|
||||
});
|
||||
// if (jobid !== "temp") {
|
||||
// const queryData = yield call(client.query, {
|
||||
// query: GET_DOC_SIZE_TOTALS,
|
||||
// fetchPolicy: "network-only",
|
||||
// variables: {
|
||||
// jobId: jobid,
|
||||
// },
|
||||
// });
|
||||
|
||||
if (
|
||||
bodyshop.jobsizelimit -
|
||||
queryData?.data?.documents_aggregate.aggregate.sum.size <=
|
||||
totalOfUploads
|
||||
) {
|
||||
//No more room... abandon ship.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
console.log("Error checking job space", error, error.stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// if (
|
||||
// bodyshop.jobsizelimit -
|
||||
// queryData?.data?.documents_aggregate.aggregate.sum.size <=
|
||||
// totalOfUploads
|
||||
// ) {
|
||||
// //No more room... abandon ship.
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// catch (error) {
|
||||
// console.log("Error checking job space", error, error.stack);
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
|
||||
try {
|
||||
@@ -259,29 +261,31 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
|
||||
let uploadResult
|
||||
try {
|
||||
uploadResult = yield new Promise((resolve, reject) => {
|
||||
|
||||
console.log("Starting XHR")
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.onprogress = (e) => {
|
||||
console.log("Upload Progress:", e.loaded, e.total);
|
||||
store.dispatch({ ...photo, progress: e.loaded / e.total, loaded: e.loaded });
|
||||
put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total, loaded: e.loaded }));
|
||||
|
||||
};
|
||||
xhr.open("PUT", preSignedUploadUrlToS3);
|
||||
xhr.setRequestHeader("Content-Type", photoBlob.type);
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable) {
|
||||
put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total, loaded: e.loaded }));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
console.log("XHR Done. Resolve promise.")
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(`Upload failed: ${xhr.statusText}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = (req, event) => {
|
||||
reject(new Error("Network error"));
|
||||
};
|
||||
|
||||
console.log("Sending XHR")
|
||||
xhr.send(photoBlob);
|
||||
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error uploading to S3", error.message, error.stack);
|
||||
@@ -395,13 +399,22 @@ function* mediaUploadCompletedAction({ payload: photos }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cancellation of uploads
|
||||
function* onMediaUploadFailure() {
|
||||
yield takeEvery(PhotosActionTypes.MEDIA_UPLOAD_FAILURE, mediaUploadFailureAction);
|
||||
}
|
||||
|
||||
function* mediaUploadFailureAction({ payload: errorMessage }) {
|
||||
Alert.alert("Upload Error", `An error occurred during upload: ${errorMessage}`);
|
||||
}
|
||||
|
||||
|
||||
export function* photosSagas() {
|
||||
yield all([
|
||||
call(onOpenImagePicker),
|
||||
call(onMediaUploadStart),
|
||||
call(onMediaUploadCompleted)
|
||||
call(onMediaUploadCompleted),
|
||||
call(onMediaUploadFailure)
|
||||
//call(onCancelUpload)
|
||||
]);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ const persistConfig = {
|
||||
key: "root",
|
||||
storage: AsyncStorage,
|
||||
// whitelist: ["photos"],
|
||||
blacklist: ["user",], // Add reducers you do NOT want to persist
|
||||
blacklist: ["user", "photos"], // Add reducers you do NOT want to persist
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"labels": {
|
||||
"error": "Error",
|
||||
"na": "N/A",
|
||||
"upload": "Upload",
|
||||
"uploadprogress": "Upload Progress"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"labels": {
|
||||
"error": "",
|
||||
"na": "",
|
||||
"upload": "",
|
||||
"uploadprogress": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"labels": {
|
||||
"error": "",
|
||||
"na": "",
|
||||
"upload": "",
|
||||
"uploadprogress": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@ const lightTheme = {
|
||||
inversePrimary: "#a5c8ff",
|
||||
elevation: {
|
||||
level0: "transparent",
|
||||
level1: "#f1f1f1ff",
|
||||
level1: "#d5d5d5ff",
|
||||
level2: "#e9eff9",
|
||||
level3: "#e1ebf6",
|
||||
level4: "#dfe9f5",
|
||||
@@ -78,7 +78,7 @@ const darkTheme = {
|
||||
inversePrimary: "#1890ff",
|
||||
elevation: {
|
||||
level0: "transparent",
|
||||
level1: "#1a1f2e",
|
||||
level1: "#575757ff",
|
||||
level2: "#212837",
|
||||
level3: "#293141",
|
||||
level4: "#2b3344",
|
||||
|
||||
Reference in New Issue
Block a user