From f1dfd4fd80932ee5578651235b7e6d85509f3fb2 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 10 May 2022 15:00:59 -0700 Subject: [PATCH] Added local media server changes. --- App.js | 1 + babel-translations.babel | 21 ++ .../local-upload-progress.component.jsx | 300 ++++++++++++++++++ .../screen-media-browser.component.jsx | 23 +- .../upload-progress.component.jsx | 2 +- google-services.json | 2 +- graphql/bodyshop.queries.js | 4 +- package.json | 4 +- redux/user/user.sagas.js | 9 +- translations/en-US/common.json | 1 + translations/es-MX/common.json | 1 + translations/fr-CA/common.json | 1 + util/local-document-upload.utility.js | 88 +++++ yarn.lock | 25 +- 14 files changed, 468 insertions(+), 14 deletions(-) create mode 100644 components/local-upload-progress/local-upload-progress.component.jsx create mode 100644 util/local-document-upload.utility.js diff --git a/App.js b/App.js index 21ba74f..e4cbd0f 100644 --- a/App.js +++ b/App.js @@ -11,6 +11,7 @@ import { persistor, store } from "./redux/store"; import "intl"; import "intl/locale-data/jsonp/en"; import "./translations/i18n"; +import "expo-asset"; Sentry.init({ dsn: "https://8d6c3de1940a4e4f8b81cf4d2150bdea@o492140.ingest.sentry.io/5558869", diff --git a/babel-translations.babel b/babel-translations.babel index ca5ae9e..2d6f8b8 100644 --- a/babel-translations.babel +++ b/babel-translations.babel @@ -1362,6 +1362,27 @@ + + localserver + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + nomedia false diff --git a/components/local-upload-progress/local-upload-progress.component.jsx b/components/local-upload-progress/local-upload-progress.component.jsx new file mode 100644 index 0000000..25090fb --- /dev/null +++ b/components/local-upload-progress/local-upload-progress.component.jsx @@ -0,0 +1,300 @@ +import { useApolloClient } from "@apollo/client"; +import * as MediaLibrary from "expo-media-library"; +import _ from "lodash"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ActivityIndicator, + Alert, + Modal, + ScrollView, + StyleSheet, + Text, + View, +} from "react-native"; +import { ProgressBar } from "react-native-paper"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.analytics"; +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, +}); + +export default connect(mapStateToProps, null)(UploadProgress); + +export function UploadProgress({ + currentUser, + bodyshop, + selectedCameraJobId, + deleteAfterUpload, + uploads, + forceRerender, +}) { + const [progress, setProgress] = useState({ + loading: false, + uploadInProgress: false, + speed: 0, + files: {}, //uri is the key, value is progress + }); + + let filesToDelete = []; + const client = useApolloClient(); + + const { t } = useTranslation(); + + useEffect(() => { + //Set the state of uploads to do. + + if (uploads) onDone(uploads); + }, [uploads]); + + //if (!uploads) return null; + + function handleOnSuccess(id, asset) { + logImEXEvent("imexmobile_successful_upload"); + filesToDelete.push(asset); + setProgress((progress) => ({ + ...progress, + action: t("mediabrowser.labels.converting"), + files: { + ...progress.files, + [id]: { + ...progress.files[id], + 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, + action: + percent === 1 + ? t("mediabrowser.labels.converting") + : t("mediabrowser.labels.uploading"), + loaded: loaded, + }, + }, + })); + } + function handleOnError(id) { + logImEXEvent("imexmobile_upload_documents_error"); + setProgress((progress) => ({ + ...progress, + action: t("mediabrowser.labels.converting"), + files: { + ...progress.files, + [id]: { + ...progress.files[id], + percent: 1, + action: t("mediabrowser.labels.converting"), + }, + }, + // }); + })); + } + + 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, + }); + + //Sequentially await the proms. + + for (var i = 0; i < data.length + 4; i = i + 4) { + 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); + } + + if (deleteAfterUpload) { + try { + const res = await Promise.all( + filesToDelete.map(async (f) => + MediaLibrary.removeAssetsFromAlbumAsync(f, f.albumId) + ) + ); + + const deleteResult = await MediaLibrary.deleteAssetsAsync( + filesToDelete + ); + + console.log("res", res); + console.log( + "🚀 ~ file: upload-progress.component.jsx ~ line 177 ~ deleteResult", + filesToDelete, + deleteResult + ); + } catch (error) { + console.log("Unable to delete picture.", error); + } + } + filesToDelete = []; + setProgress({ + loading: false, + speed: 0, + action: null, + + uploadInProgress: false, + files: {}, //uri is the key, value is progress + }); + + forceRerender(); + }; + + const CreateUploadProm = async (p) => { + let filename; + filename = p.filename || p.uri.split("/").pop(); + + await handleLocalUpload({ + ev: { + filename, + mediaId: p.id, + onError: () => handleOnError(p.id), + onProgress: ({ percent, loaded }) => + handleOnProgress(p.id, percent, loaded), + onSuccess: () => handleOnSuccess(p.id, p), + file: p, + }, + context: { + bodyshop: bodyshop, + jobid: + selectedCameraJobId !== "temp" ? selectedCameraJobId : "temporary", + }, + }); + + //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 ( + { + Alert.alert("Modal has been closed."); + }} + > + + {progress.loading && } + {progress.action && ( + {`${progress.action} ${ + (progress.speed !== 0 || !progress.speed) && + `- ${formatBytes(progress.speed)}/sec` + }`} + )} + + {Object.keys(progress.files).map((key) => ( + + + {progress.files[key].filename} + + + + + + ))} + + + + ); +} +const styles = StyleSheet.create({ + modal: { + flex: 1, + marginTop: 50, + marginBottom: 60, + 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: { + flex: 1, + // 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, + }, +}); diff --git a/components/screen-media-browser/screen-media-browser.component.jsx b/components/screen-media-browser/screen-media-browser.component.jsx index 9c6d9e0..edd33f2 100644 --- a/components/screen-media-browser/screen-media-browser.component.jsx +++ b/components/screen-media-browser/screen-media-browser.component.jsx @@ -12,12 +12,15 @@ import JobSpaceAvailable from "../job-space-available/job-space-available.compon import UploadDeleteSwitch from "../upload-delete-switch/upload-delete-switch.component"; import UploadProgress from "../upload-progress/upload-progress.component"; import { MediaType } from "expo-media-library"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import LocalUploadProgress from "../local-upload-progress/local-upload-progress.component"; const mapStateToProps = createStructuredSelector({ selectedCameraJobId: selectCurrentCameraJobId, + bodyshop: selectBodyshop, }); -export function ImageBrowserScreen({ selectedCameraJobId }) { +export function ImageBrowserScreen({ bodyshop, selectedCameraJobId }) { const { t } = useTranslation(); const [uploads, setUploads] = useState(null); const [tick, setTick] = useState(0); @@ -49,7 +52,7 @@ export function ImageBrowserScreen({ selectedCameraJobId }) { initialLoad: 100, assetsType: [MediaType.photo, MediaType.video], minSelection: 1, - maxSelection: 3, + // maxSelection: 3, portraitCols: 4, landscapeCols: 4, }), @@ -120,7 +123,15 @@ export function ImageBrowserScreen({ selectedCameraJobId }) { return ( - + {bodyshop.uselocalmediaserver ? ( + + {t("mediabrowser.labels.localserver", { + url: bodyshop.localmediaserverhttp, + })} + + ) : ( + + )} {!selectedCameraJobId && ( )} - + {bodyshop.uselocalmediaserver ? ( + + ) : ( + + )} ); } diff --git a/components/upload-progress/upload-progress.component.jsx b/components/upload-progress/upload-progress.component.jsx index e96a30b..1337f76 100644 --- a/components/upload-progress/upload-progress.component.jsx +++ b/components/upload-progress/upload-progress.component.jsx @@ -152,7 +152,7 @@ export function UploadProgress({ return; } } - console.log("DATA", data); + //Sequentially await the proms. for (var i = 0; i < data.length + 4; i = i + 4) { diff --git a/google-services.json b/google-services.json index 162fc0e..ddde65b 100644 --- a/google-services.json +++ b/google-services.json @@ -44,4 +44,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/graphql/bodyshop.queries.js b/graphql/bodyshop.queries.js index cb1e2f8..7e4aba2 100644 --- a/graphql/bodyshop.queries.js +++ b/graphql/bodyshop.queries.js @@ -6,9 +6,9 @@ export const QUERY_BODYSHOP = gql` id jobsizelimit md_ro_statuses - + uselocalmediaserver + localmediaserverhttp shopname - features } } diff --git a/package.json b/package.json index 0b0cf90..ae7b1a9 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,13 @@ "expo-file-system": "~13.1.4", "expo-firebase-analytics": "~6.0.1", "expo-font": "~10.0.5", + "expo-image-manipulator": "~10.2.0", "expo-images-picker": "^2.2.5", "expo-localization": "~12.0.1", "expo-media-library": "~14.0.1", "expo-permissions": "~13.1.1", "expo-status-bar": "~1.2.0", - "expo-updates": "~0.11.6", + "expo-updates": "~0.11.7", "expo-video-thumbnails": "~6.1.0", "firebase": "8.2.3", "formik": "^2.2.9", @@ -49,6 +50,7 @@ "lodash": "^4.17.20", "luxon": "^2.3.1", "moment": "^2.29.1", + "normalize-url": "^7.0.3", "react": "17.0.1", "react-dom": "17.0.1", "react-i18next": "^11.15.5", diff --git a/redux/user/user.sagas.js b/redux/user/user.sagas.js index cf31186..2fe155c 100644 --- a/redux/user/user.sagas.js +++ b/redux/user/user.sagas.js @@ -108,7 +108,14 @@ export function* signInSuccessSaga({ payload }) { const shop = yield client.query({ query: QUERY_BODYSHOP }); logImEXEvent("imexmobile_sign_in_success", payload); - yield put(setBodyshop(shop.data.bodyshops[0])); + yield put( + setBodyshop({ + ...shop.data.bodyshops[0], + uselocalmediaserver: true, + localmediaserverhttp: "http://192.168.1.235:8000", //TODO: ENSURE THAT THIS HAS BEEN REMOVED POST TESTING. + localmediaservernetwork: "\\192.168.1.235:8000", //TODO: ENSURE THAT THIS HAS BEEN REMOVED POST TESTING. + }) + ); } catch (error) { console.log("UH-OH. Couldn't get shop details.", error); } diff --git a/translations/en-US/common.json b/translations/en-US/common.json index 2ac41ca..310e803 100644 --- a/translations/en-US/common.json +++ b/translations/en-US/common.json @@ -92,6 +92,7 @@ "labels": { "converting": "Converting", "deleteafterupload": "Delete After Upload", + "localserver": "Local Server URL: {{url}}", "nomedia": "Look's like there's no media on your device. Take some photos or videos and they will appear here.", "selectjob": "--- Select a job ---", "selectjobassetselector": "Please select a job to upload media. ", diff --git a/translations/es-MX/common.json b/translations/es-MX/common.json index 1cf5001..ddd9edc 100644 --- a/translations/es-MX/common.json +++ b/translations/es-MX/common.json @@ -92,6 +92,7 @@ "labels": { "converting": "", "deleteafterupload": "", + "localserver": "", "nomedia": "", "selectjob": "", "selectjobassetselector": "", diff --git a/translations/fr-CA/common.json b/translations/fr-CA/common.json index 0290d22..c74df25 100644 --- a/translations/fr-CA/common.json +++ b/translations/fr-CA/common.json @@ -92,6 +92,7 @@ "labels": { "converting": "", "deleteafterupload": "", + "localserver": "", "nomedia": "", "selectjob": "", "selectjobassetselector": "", diff --git a/util/local-document-upload.utility.js b/util/local-document-upload.utility.js new file mode 100644 index 0000000..c9789cd --- /dev/null +++ b/util/local-document-upload.utility.js @@ -0,0 +1,88 @@ +import axios from "axios"; +import { store } from "../redux/store"; + +import * as MediaLibrary from "expo-media-library"; +import * as ImageManipulator from "expo-image-manipulator"; + +export const handleLocalUpload = async ({ ev, context }) => { + const { onError, onSuccess, onProgress, filename, mediaId } = ev; + const { jobid, invoice_number, vendorid, callbackAfterUpload } = context; + + var options = { + headers: { "X-Requested-With": "XMLHttpRequest" }, + onUploadProgress: (e) => { + if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 }); + }, + }; + + const formData = new FormData(); + + formData.append("jobid", jobid); + if (invoice_number) { + formData.append("invoice_number", invoice_number); + formData.append("vendorid", vendorid); + } + const imageData = await MediaLibrary.getAssetInfoAsync(mediaId); + + const newFile = await (await fetch(imageData.localUri)).blob(); + let thumb; + let fileData = { + uri: null, + type: null, + name: null, + }; + if (newFile.type === "image/heic") { + try { + thumb = await ImageManipulator.manipulateAsync(imageData.uri, [], { + format: "jpeg", + base64: true, + compress: 0.75, + }); + const name = newFile.data.name.split("."); + name.pop(); + fileData = { + uri: thumb.uri, + type: newFile.type, + name: name.join("") + ".jpeg", + }; + } catch (error) { + console.log(error); + } + } else { + fileData = { + uri: imageData.localUri, + type: newFile.type, + name: filename, + }; + } + + formData.append("file", fileData); + const bodyshop = store.getState().user.bodyshop; + + try { + const imexMediaServerResponse = await axios.post( + `${bodyshop.localmediaserverhttp}/${ + invoice_number ? "bills" : "jobs" + }/upload`, + formData, + { + ...options, + } + ); + + if (imexMediaServerResponse.status !== 200) { + if (onError) { + console.log(imexMediaServerResponse); + onError(imexMediaServerResponse.statusText); + } + } else { + onSuccess && onSuccess(); + } + + if (callbackAfterUpload) { + callbackAfterUpload(); + } + } catch (error) { + console.log("Error uploading documents:", error); + } +}; diff --git a/yarn.lock b/yarn.lock index 0285ec3..daf2cc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4440,6 +4440,11 @@ expo-image-loader@~3.0.0: resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-3.0.0.tgz#5ada47a0f90f8dec1777520d36e35c65155d9ea9" integrity sha512-r4D+uLCf5vm5A2JIbF1Bc9FjYKrYGSLShbFB1MUvZ4BpSXJPRsprYZ9veUBVzzhh8hr23ahTFjMzp3nC57iREw== +expo-image-loader@~3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-3.1.1.tgz#f88d94e66c5a102f15d858973c03b92e10662575" + integrity sha512-ZX4Bh3K4CCX1aZflnmbOgFNLS+c0/GUys4wdvqxO+4A4KU1NNb3jE7RVa/OFYNPDcGhEw20c1QjyE/WsVURJpg== + expo-image-manipulator@~10.1.2: version "10.1.2" resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-10.1.2.tgz#1053d26f30d5c690ebe2d012b98ac3f0ae57b51a" @@ -4448,6 +4453,13 @@ expo-image-manipulator@~10.1.2: expo-image-loader "~3.0.0" expo-modules-core "~0.4.4" +expo-image-manipulator@~10.2.0: + version "10.2.1" + resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-10.2.1.tgz#6fd0db248f10a5e99b16e1f53d382ca77e660b4a" + integrity sha512-0klgPMn8fIUkbWpRVT0LVCtq0ozzm3gO60jZEcJPofJRQWDKuv3Rcf0+8pTqpn45J53eAGsuZ72/Yj0AJqaedQ== + dependencies: + expo-image-loader "~3.1.0" + expo-images-picker@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/expo-images-picker/-/expo-images-picker-2.2.5.tgz#7c3b9969fc11800cf481ff126ef2dbac25ac54a0" @@ -4551,10 +4563,10 @@ expo-updates-interface@~0.5.0: resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-0.5.0.tgz#30b05b9e190b3e2662d7cc26cd84d305d7ab4217" integrity sha512-3Yhip5LQ6x1nQ/2Xm/uP3Oeann7YkaBwsdUpxbcMtn2Ayucuu9U7r9ltwzBFxC4RWebfhXGJZ5+gx85y0leGXQ== -expo-updates@~0.11.6: - version "0.11.6" - resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.11.6.tgz#5541f2f791d51cd51c4a37a3e241c8d167226d2d" - integrity sha512-nTzEc/z0/QHwu6gJhYSh5TWDSzNLO9bmtP4aQzStfqT8RRoh1bYRomszxjc7e3CsZT8xrG88XKlZ4iKL6zHLoQ== +expo-updates@~0.11.7: + version "0.11.7" + resolved "https://registry.yarnpkg.com/expo-updates/-/expo-updates-0.11.7.tgz#c267ce8818d42a997d49a1e24b11b4887a2fb17b" + integrity sha512-zmteCFOBj2OtDOZO5eGgFHR4EXZvFUv5DM56aMkZ6+PE/fo+8ZjNZLxkQWD33GXmXs/9jLCLKXPj2+6kCJvyhg== dependencies: "@expo/config" "^6.0.6" "@expo/config-plugins" "^4.0.2" @@ -6542,6 +6554,11 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-url@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-7.0.3.tgz#12e56889f7a54b2d5b09616f36c442a9063f61af" + integrity sha512-RiCOdwdPnzvwcBFJE4iI1ss3dMVRIrEzFpn8ftje6iBfzBInqlnRrNhxcLwBEKjPPXQKzm1Ptlxtaiv9wdcj5w== + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz"