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"