From 83fe7059e93e191642a1615573802b05d6dcc519 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 7 Oct 2025 13:40:37 -0700 Subject: [PATCH] WIP replace image selector --- .../screen-media-browser.component.jsx | 211 +++-------------- .../upload-progress.component.jsx | 152 ++++++------ util/document-upload.utility.js | 222 ++---------------- 3 files changed, 134 insertions(+), 451 deletions(-) diff --git a/components/screen-media-browser/screen-media-browser.component.jsx b/components/screen-media-browser/screen-media-browser.component.jsx index 88beb65..5caad83 100644 --- a/components/screen-media-browser/screen-media-browser.component.jsx +++ b/components/screen-media-browser/screen-media-browser.component.jsx @@ -1,7 +1,7 @@ import { Ionicons } from "@expo/vector-icons"; //import { AssetsSelector } from "expo-images-picker"; import { MediaType } from "expo-media-library"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { StyleSheet, Text, View } from "react-native"; import { connect } from "react-redux"; @@ -20,7 +20,7 @@ import UploadDeleteSwitch from "../upload-delete-switch/upload-delete-switch.com import UploadProgress from "../upload-progress/upload-progress.component"; import { SegmentedButtons } from "react-native-paper"; import * as ImagePicker from "expo-image-picker"; - import { Button } from "react-native-paper"; +import { Button } from "react-native-paper"; // import * as MediaLibrary from "expo-media-library"; const mapStateToProps = createStructuredSelector({ @@ -50,120 +50,39 @@ export function ImageBrowserScreen({ setTick((tick) => tick + 1); }, []); + const [percentage, setPercentage] = useState(0); + + useEffect(() => { + (async () => { + if (Constants.platform.ios) { + const cameraRollStatus = + await ImagePicker.requestMediaLibraryPermissionsAsync(); + const cameraStatus = await ImagePicker.requestCameraPermissionsAsync(); + if ( + cameraRollStatus.status !== "granted" || + cameraStatus.status !== "granted" + ) { + alert("Sorry, we need these permissions to make this work!"); + } + } + })(); + }, []); + + const pickImage = async () => { + let result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ["images", "videos"], + aspect: [4, 3], + quality: 1, + allowsMultipleSelection: true, + }); + setUploads(result.assets); + }; const onDone = (data) => { logImEXEvent("imexmobile_upload_documents", { count: data.length }); - // const uploads = await Promise.all( - // data.map(async (item) => { - // let id = item.id || item.fileName; - // if (!item.id && item.uri) { - // id = await getAssetIdFromUri(item.uri, item.fileName); - // } - // return { - // ...item, - // localUri: item.uri, - // id, - // }; - // }) - // ); - // console.log("onDone", uploads); + if (data.length !== 0) setUploads(data); }; - const widgetErrors = useMemo( - () => ({ - errorTextColor: "black", - errorMessages: { - hasErrorWithPermissions: "Please Allow media gallery permissions.", - hasErrorWithLoading: "There was an error while loading images.", - hasErrorWithResizing: "There was an error while loading images.", - hasNoAssets: "No images found.", - }, - }), - [] - ); - - const widgetSettings = useMemo( - () => ({ - getImageMetaData: false, // true might perform slower results but gives meta data and absolute path for ios users - initialLoad: 50, - assetsType: [MediaType.photo, MediaType.video], - minSelection: 1, - // maxSelection: 3, - portraitCols: density, - landscapeCols: density, - }), - [density] - ); - - const widgetNavigator = useMemo( - () => ({ - Texts: { - finish: t("mediabrowser.actions.upload"), - back: t("mediabrowser.actions.refresh"), - selected: "selected", - }, - midTextColor: "black", - minSelection: 1, - buttonTextStyle: styles.textStyle, - buttonStyle: styles.buttonStyle, - onBack: () => { - forceRerender(); - }, - onSuccess: onDone, - }), - [] - ); - - const widgetStyles = useMemo( - () => ({ - margin: 2, - bgColor: "white", - spinnerColor: "blue", - widgetWidth: 99, - videoIcon: { - Component: Ionicons, - iconName: "videocam", - color: "white", - size: 20, - }, - selectedIcon: { - Component: Ionicons, - iconName: "checkmark-circle-outline", - color: "white", - bg: "rgba(35,35,35, 0.75)", - size: 32, - }, - }), - [] - ); - - // const handleSelectPhotos = async () => { - // let result = await ImagePicker.launchImageLibraryAsync({ - // mediaTypes: ["images", "videos"], - // allowsMultipleSelection: true, - // // aspect: [4, 3], - // }); - // console.log("*** ~ handleSelectPhotos ~ result:", result); - - // if (!result.canceled) { - // const uploads = await Promise.all( - // result.assets.map(async (item) => { - // let id = item.id || item.fileName; - // if (!item.id && item.uri) { - // id = await getAssetIdFromUri(item.uri); - // } - // return { - // ...item, - // localUri: item.uri, - // id, - // }; - // }) - // ); - // console.log("Uploads from handleSelectPhotos", uploads); - // setUploads(uploads); - // } - // }; - return ( @@ -208,18 +127,7 @@ export function ImageBrowserScreen({ {t("mediabrowser.labels.selectjobassetselector")} )} - {/* {selectedCameraJobId && ( - - )} */} - - + {bodyshop.uselocalmediaserver ? ( { -// forceRerender(); -// }, -// doneFunction: onDone, -// }, - -// noAssets: { -// Component: function NoAsset() { -// return ( -// -// -// -// {t("mediabrowser.labels.nomedia")} -// -// -// ); -// }, -// }, -// }} - // // Utility to get asset ID from URI if missing // async function getAssetIdFromUri(uri, filename = null, maxPages = 10) { // let after = null; diff --git a/components/upload-progress/upload-progress.component.jsx b/components/upload-progress/upload-progress.component.jsx index bcdfc74..d0388ae 100644 --- a/components/upload-progress/upload-progress.component.jsx +++ b/components/upload-progress/upload-progress.component.jsx @@ -1,5 +1,5 @@ import { useApolloClient } from "@apollo/client"; -import * as FileSystem from "expo-file-system"; +import { File } from "expo-file-system"; import * as MediaLibrary from "expo-media-library"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -127,81 +127,97 @@ export function UploadProgress({ statusText: "Preparing upload...", }; }); + try { + //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. + if (acc.fileSize) { + return acc + acc.fileSize; + } else { + const info = new File(val.uri).size; + data.push({ ...info, ...val }); //Add in the size. + val.albumId && MediaLibrary.migrateAlbumIfNeededAsync(val.albumId); + return (await acc) + info.size; + } + }, 0); - //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. - if (acc.fileSize) { - return acc + acc.fileSize; - } else { - 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 (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. + 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, - speed: 0, - action: null, + totalToUpload: totalOfUploads, + totalUploaded: 0, + totalFilesCompleted: 0, + startTime: new Date(), + totalFiles: data.length, + currentFile: null, statusText: null, - uploadInProgress: false, + files: {}, //uri is the key, value is progress })); - 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); + 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); + } + } catch (error) { + console.log("Error during upload.", error, error.stack); + Sentry.captureException(error); + setProgress((progress) => ({ + ...progress, + speed: 0, + action: null, + statusText: null, + uploadInProgress: false, + })); + Alert.alert( + t("mediabrowser.labels.uploaderror_title"), + t("mediabrowser.labels.uploaderror") + ); + return; } //Everything is uploaded, delete the succesful ones. diff --git a/util/document-upload.utility.js b/util/document-upload.utility.js index 5aa8561..0e5a592 100644 --- a/util/document-upload.utility.js +++ b/util/document-upload.utility.js @@ -6,7 +6,7 @@ import { client } from "../graphql/client"; import { INSERT_NEW_DOCUMENT } from "../graphql/documents.queries"; import { axiosAuthInterceptorId } from "./CleanAxios"; //import { splitClient } from "../components/screen-main/screen-main.component"; -import * as FileSystem from "expo-file-system"; +import { File } from "expo-file-system"; //Context: currentUserEmail, bodyshop, jobid, invoiceid @@ -16,55 +16,35 @@ cleanAxios.interceptors.request.eject(axiosAuthInterceptorId); export const handleUpload = async (ev, context) => { const { mediaId, onError, onSuccess, onProgress } = ev; - const { bodyshop, jobId } = context; + const { bodyshop, jobId, photo } = context; try { + console.log("**** I GOT HERE AT LEAST ****") const imageData = await MediaLibrary.getAssetInfoAsync(mediaId); const imageUri = imageData.localUri || imageData.uri const newFile = await ( await fetch(imageUri) ).blob(); - let extension = imageData.filename.split(".").pop(); - //Default to Cloudinary in case of split treatment errors. - let destination = - splitClient?.getTreatment("Imgproxy") === "on" ? "imgproxy" : "cloudinary"; + let key = `${bodyshop.id}/${jobId}/${replaceAccents( + imageData.filename || imageUri.split("/").pop() + ).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}` - let key = - destination === "imgproxy" - ? `${bodyshop.id}/${jobId}/${replaceAccents( - imageData.filename || imageUri.split("/").pop() - ).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}` - : `${bodyshop.id}/${jobId}/${( - imageData.filename || imageUri.split("/").pop() - ).replace(/\.[^/.]+$/, "")}-${new Date().getTime()}`; const res = - destination === "imgproxy" - ? await uploadToImgproxy( - key, - mediaId, - imageData, - extension, - newFile.type, //Filetype - newFile, //File - onError, - onSuccess, - onProgress, - context - ) - : await uploadToCloudinary( - key, - mediaId, - imageData, - extension, - newFile.type, //Filetype - newFile, //File - onError, - onSuccess, - onProgress, - context - ); + await uploadToImgproxy( + key, + mediaId, + imageData, + extension, + newFile.type, //Filetype + newFile, //File + onError, + onSuccess, + onProgress, + context + ) + return res; } catch (error) { console.log("Error creating upload promise", error.message, error.stack); @@ -79,34 +59,6 @@ export const handleUpload = async (ev, context) => { } }; -export const handleUploadImgproxy = async (ev, context) => { - const { mediaId, onError, onSuccess, onProgress } = ev; - const { bodyshop, jobId } = context; - const imageData = await MediaLibrary.getAssetInfoAsync(mediaId); - const imageUri = imageData.localUri || imageData.uri - const newFile = await ( - await fetch(imageUri) - ).blob(); - let extension = imageUri.split(".").pop(); - let key = `${bodyshop.id}/${jobId}/${( - imageData.filename || imageUri.split("/").pop() - ).replace(/\.[^/.]+$/, "")}-${new Date().getTime()}`; - - const res = await uploadToImgproxy( - key, - mediaId, - imageData, - extension, - newFile.type, //Filetype - newFile, //File - onError, - onSuccess, - onProgress, - context - ); - return res; -}; - export const uploadToImgproxy = async ( key, mediaId, @@ -230,142 +182,6 @@ export const uploadToImgproxy = async ( return { success: true, mediaId }; }; -export const uploadToCloudinary = async ( - key, - mediaId, - imageData, - extension, - fileType, - file, - onError, - onSuccess, - onProgress, - context -) => { - const { bodyshop, jobId, uploaded_by } = context; - - //Set variables for getting the signed URL. - let timestamp = Math.floor(Date.now() / 1000); - let public_id = key; - const upload_preset = fileType.startsWith("video") - ? "incoming_upload_video" - : "incoming_upload"; - - //Get the signed url. - let signedURLResponse; - try { - signedURLResponse = await axios.post(`${env.API_URL}/media/sign`, { - public_id: public_id, - timestamp: timestamp, - upload_preset: upload_preset, - }); - } catch (error) { - console.log("ERROR GETTING SIGNED URL", error); - Sentry.captureException(error); - - return { success: false, error: error }; - } - - if (signedURLResponse.status !== 200) { - console.log("Error Getting Signed URL", signedURLResponse.statusText); - if (onError) onError(signedURLResponse.statusText); - return { success: false, error: signedURLResponse.statusText }; - } - - //Build request to end to cloudinary. - - var signature = signedURLResponse.data; - var options = { - headers: { - "X-Requested-With": "XMLHttpRequest", - "Content-Type": "multipart/form-data", - }, - onUploadProgress: (e) => { - if (onProgress) - onProgress({ percent: e.loaded / e.total, loaded: e.loaded }); - }, - }; - - const formData = new FormData(); - formData.append("file", { - uri: imageData.localUri, - type: fileType, - name: file.data.name, - }); - formData.append("upload_preset", upload_preset); - formData.append("api_key", env.REACT_APP_CLOUDINARY_API_KEY); - formData.append("public_id", public_id); - formData.append("timestamp", timestamp); - formData.append("signature", signature); - - //Upload request to Cloudinary - let cloudinaryUploadResponse; - try { - cloudinaryUploadResponse = await cleanAxios.post( - `${env.REACT_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType( - fileType - )}/upload`, - formData, - options - ); - } catch (error) { - console.log("CLOUDINARY error", error.response, cloudinaryUploadResponse); - Sentry.captureException(error); - - if (onError) onError(error.message); - return { success: false, error: error }; - } - - if (cloudinaryUploadResponse.status !== 200) { - console.log( - "Error uploading to cloudinary.", - cloudinaryUploadResponse.statusText, - cloudinaryUploadResponse - ); - if (onError) onError(cloudinaryUploadResponse.statusText); - return { success: false, error: cloudinaryUploadResponse.statusText }; - } - - //Insert the document with the matching key. - const documentInsert = await client.mutate({ - mutation: INSERT_NEW_DOCUMENT, - variables: { - docInput: [ - { - ...(jobId ? { jobid: jobId } : {}), - uploaded_by: uploaded_by, - key: key, - type: fileType, - extension: extension, - bodyshopid: bodyshop.id, - size: cloudinaryUploadResponse.data.bytes || file.size, - ...(imageData.creationTime - ? { takenat: new Date(imageData.creationTime) } - : {}), - }, - ], - }, - }); - if (!documentInsert.errors) { - if (onSuccess) - onSuccess({ - uid: documentInsert.data.insert_documents.returning[0].id, - name: documentInsert.data.insert_documents.returning[0].name, - status: "done", - key: documentInsert.data.insert_documents.returning[0].key, - }); - } else { - if (onError) onError(JSON.stringify(documentInsert.errors)); - return { - success: false, - error: JSON.stringify(documentInsert.errors), - mediaId, - }; - } - - return { success: true, mediaId }; -}; - export function DetermineFileType(filetype) { if (!filetype) return "auto"; else if (filetype.startsWith("image")) return "image";