From 4950f7850d57e2e605081f6139da4d6599d91426 Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Mon, 7 Nov 2022 22:36:39 -0600 Subject: [PATCH 1/8] Use FastImage --- example/globals.d.ts | 1 + example/ios/Podfile.lock | 29 +++++++++++++++++++++++++++++ example/package.json | 1 + example/src/App.tsx | 22 ++++++++++++---------- example/src/img/placeholder.png | Bin 0 -> 1775 bytes example/yarn.lock | 5 +++++ 6 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 example/globals.d.ts create mode 100644 example/src/img/placeholder.png diff --git a/example/globals.d.ts b/example/globals.d.ts new file mode 100644 index 0000000..e2937d4 --- /dev/null +++ b/example/globals.d.ts @@ -0,0 +1 @@ +declare module '*.png'; diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6a36ee6..42a7fec 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -75,6 +75,15 @@ PODS: - glog (0.3.5) - hermes-engine (0.70.3) - libevent (2.1.12) + - libwebp (1.2.3): + - libwebp/demux (= 1.2.3) + - libwebp/mux (= 1.2.3) + - libwebp/webp (= 1.2.3) + - libwebp/demux (1.2.3): + - libwebp/webp + - libwebp/mux (1.2.3): + - libwebp/demux + - libwebp/webp (1.2.3) - OpenSSL-Universal (1.1.1100) - RCT-Folly (2021.07.22.00): - boost @@ -370,8 +379,18 @@ PODS: - React-jsi (= 0.70.3) - React-logger (= 0.70.3) - React-perflogger (= 0.70.3) + - RNFastImage (8.6.3): + - React-Core + - SDWebImage (~> 5.11.1) + - SDWebImageWebPCoder (~> 0.8.4) - RNReactNativeHapticFeedback (1.14.0): - React-Core + - SDWebImage (5.11.1): + - SDWebImage/Core (= 5.11.1) + - SDWebImage/Core (5.11.1) + - SDWebImageWebPCoder (0.8.5): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.0) - Yoga (1.14.0) - YogaKit (1.18.1): @@ -437,6 +456,7 @@ DEPENDENCIES: - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - RNFastImage (from `../node_modules/react-native-fast-image`) - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -454,7 +474,10 @@ SPEC REPOS: - FlipperKit - fmt - libevent + - libwebp - OpenSSL-Universal + - SDWebImage + - SDWebImageWebPCoder - SocketRocket - YogaKit @@ -527,6 +550,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNFastImage: + :path: "../node_modules/react-native-fast-image" RNReactNativeHapticFeedback: :path: "../node_modules/react-native-haptic-feedback" Yoga: @@ -551,6 +576,7 @@ SPEC CHECKSUMS: glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: bb344d89a0d14c2c91ad357480a79698bb80e186 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCTRequired: 5cf7e7d2f12699724b59f90350257a422eaa9492 @@ -580,7 +606,10 @@ SPEC CHECKSUMS: React-RCTVibration: b9a58ffdd18446f43d493a4b0ecd603ee86be847 React-runtimeexecutor: e9b1f9310158a1e265bcdfdfd8c62d6174b947a2 ReactCommon: 01064177e66d652192c661de899b1076da962fd9 + RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c + SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d + SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 Yoga: 2ed968a4f060a92834227c036279f2736de0fce3 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/example/package.json b/example/package.json index dd52f2f..e2753a8 100644 --- a/example/package.json +++ b/example/package.json @@ -12,6 +12,7 @@ "dependencies": { "react": "18.1.0", "react-native": "0.70.3", + "react-native-fast-image": "8.6.3", "react-native-haptic-feedback": "1.14.0", "react-native-image-picker": "4.10.0", "react-native-sortable-grid": "https://github.com/rossmartin/react-native-sortable-grid.git#b5c911c263b8c230c4973af00986724bcb234929" diff --git a/example/src/App.tsx b/example/src/App.tsx index 5cfa072..64ca69d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -3,7 +3,6 @@ import { ActivityIndicator, Animated, Button, - ImageBackground, Pressable, SafeAreaView, StyleSheet, @@ -15,10 +14,12 @@ import SortableGrid, { ItemOrder } from 'react-native-sortable-grid'; import ReactNativeHapticFeedback, { HapticOptions, } from 'react-native-haptic-feedback'; +import FastImage from 'react-native-fast-image'; import ProgressBar from './components/ProgressBar'; import useFileUpload, { UploadItem } from '../../src/index'; import { allSettled } from './util/allSettled'; +import placeholderImage from './img/placeholder.png'; const hapticFeedbackOptions: HapticOptions = { enableVibrateFallback: false, @@ -114,7 +115,6 @@ export default function App() { const response = await launchImageLibrary({ mediaType: 'photo', selectionLimit: 0, - quality: 0.8, }); const items: Item[] = @@ -215,12 +215,13 @@ export default function App() { const showProgress = !item.failed && itemProgress > 0 && itemProgress < 100; return ( - + + {showProgress ? ( ) : null} @@ -233,7 +234,7 @@ export default function App() { - + ); }; @@ -271,11 +272,12 @@ const styles = StyleSheet.create({ }, imageBackground: { flex: 1, + margin: 8, justifyContent: 'center', alignItems: 'center', - margin: 8, }, image: { + ...StyleSheet.absoluteFillObject, borderRadius: 12, }, deleteIcon: { diff --git a/example/src/img/placeholder.png b/example/src/img/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..df321bb2b54d58325609003e072d916508a44b61 GIT binary patch literal 1775 zcmeAS@N?(olHy`uVBq!ia0vp^%Yk?S2OE&g6s`$jU|@{TbaoE#baqwWn z(3n^{!O@#3P~iCg=&lK(PO_(z)B(TSSk*nanFNW|NXd9L60ISL4t z@HV~Up1bEOW7i_Sxr_7z-Yk7$>c9HxqDr3s^7U!Yc$rIDOpf`;HxwP+cFFtm#NIb) zDj%8@m|h&#NSYOx+O=xg3YPn~jlq4u6d)0FGf`d;bQ zEo?pgY~9DL%{TU0$48!?(5BhGvB7o83)cyXlMH<&)u(Iyd+BZRYtjwI!;Csp0vrU* zd;?A-8cbA=b7W;p>JU0TW66}Gd+YCC{QT`+!_3HEtB!x(wvO>Rlf;MWdp86wSaZa* z&P>a+zdki4zSLUJ-0}s(zvOfA*BcL51nhtQ=kCKjb$lguOs3z3FA61T9b zrzemOPA*DK%`48xFZsulxB_U@wak!+k_cZPtK|G#y~LFKq*T3%+yamm2Ac{iATu|$ zBvGLvHz%*ys=`(YtilS&1_|pcDS(xfWZNo5_y#CA=NF|anCThl87SFtDJUq|6s4qD z1-ZCEwF7y!N*N_31y=g{<>lpi<;HsXMd|v6mX?EaW<_dFq)TQlFnEA=*cqA`*nrhz$RKP)(iwrQ(;QhRk_@`e(Bjl0=loot zl_hqFn1O0WmV|1w(Fge)DI_4l3>F23vmKX>K0MCsxY#`MW&@K5yQhm|NCo5DOOCt@ z3IZ&K2L$;4WH+;jetw~O<4ozh+IIz9EZl1zs7)66!TNwjF`D7rn{1xu$&ERT7Ms@_ z^7yfBVtsIC-!TIV$rEA?#=oTwKS)s+RWup|Ln{?*V6RxkbivYW$6HX*=IQF^vd$@? F2>`IysG9%) literal 0 HcmV?d00001 diff --git a/example/yarn.lock b/example/yarn.lock index c6fe38b..21263de 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -3743,6 +3743,11 @@ react-native-codegen@^0.70.5: jscodeshift "^0.13.1" nullthrows "^1.1.1" +react-native-fast-image@8.6.3: + version "8.6.3" + resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255" + integrity sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg== + react-native-gradle-plugin@^0.70.3: version "0.70.3" resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8" From 5bc8f58a10f195c51f8bc919cfd304ae58fb5479 Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Tue, 8 Nov 2022 19:36:37 -0600 Subject: [PATCH 2/8] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4b8559..2c9b09d 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ useFileUpload({ headers }); Requests will time out if you background the app. This can be addressed by using [react-native-background-upload](https://github.com/Vydia/react-native-background-upload). -The React Native team did a a heavy lift to polyfill and bridge `XMLHttpRequest` to the native side for us. Hopefully some day it is updated to support requests while an app is backgrounded. +The React Native team did a a heavy lift to polyfill and bridge `XMLHttpRequest` to the native side for us. [There is an open PR in React Native to allow network requests to run in the background for iOS](https://github.com/facebook/react-native/pull/31838). There are plans to have a similar PR for Android as well. `react-native-background-upload` is great but if backgrounding can be supported without any native dependencies it is a win for everyone. ### Why send 1 file at a time instead of multiple in a single request? From e53d0e9cc8532fb0afb3b5443d16e31faf1e501f Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Tue, 8 Nov 2022 19:44:08 -0600 Subject: [PATCH 3/8] Reject promise when request is aborted --- src/hooks/useFileUpload.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/hooks/useFileUpload.ts b/src/hooks/useFileUpload.ts index d01aabf..557d0ab 100644 --- a/src/hooks/useFileUpload.ts +++ b/src/hooks/useFileUpload.ts @@ -70,6 +70,15 @@ export default function useFileUpload({ reject(result); }; + xhr.onabort = () => { + const result: OnErrorData = { + item, + error: 'Request aborted', + }; + onError?.(result); + reject(result); + }; + headers?.forEach((value: string, key: string) => { xhr.setRequestHeader(key, value); }); From 5d535945a796ecf1eb8c3722e01949f60c369aee Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:01:08 -0600 Subject: [PATCH 4/8] Nit for progress --- example/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 64ca69d..0619dd5 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -44,7 +44,7 @@ export default function App() { timeout: 60000, // you can set this lower to cause timeouts to happen onProgress: ({ item, event }) => { const progress = event?.loaded - ? Math.floor((event.loaded / event.total) * 100) + ? Math.round((event.loaded / event.total) * 100) : 0; updateItem({ item, From 1fba6c2c487beb50a22fe9c910409df2c8f6d453 Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:54:37 -0600 Subject: [PATCH 5/8] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c9b09d..b7487c6 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ startUpload({ ### `abortUpload` -Abort a file upload for a given file. +Abort a file upload for a given file. The promise from `startUpload` gets rejected and `onError` runs if present. ```ts // Pass the uri of a file that started uploading From 589b0d7f31ac653b6e0f7799daaa77dcafcb8177 Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Tue, 8 Nov 2022 21:41:17 -0600 Subject: [PATCH 6/8] Update example for FastImage --- example/src/App.tsx | 99 ++++++++++++++----- .../src/util/{allSettled.ts => general.ts} | 3 + 2 files changed, 77 insertions(+), 25 deletions(-) rename example/src/util/{allSettled.ts => general.ts} (73%) diff --git a/example/src/App.tsx b/example/src/App.tsx index 0619dd5..5d3f42b 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -17,8 +17,8 @@ import ReactNativeHapticFeedback, { import FastImage from 'react-native-fast-image'; import ProgressBar from './components/ProgressBar'; -import useFileUpload, { UploadItem } from '../../src/index'; -import { allSettled } from './util/allSettled'; +import useFileUpload, { UploadItem, OnProgressData } from '../../src/index'; +import { allSettled, sleep } from './util/general'; import placeholderImage from './img/placeholder.png'; const hapticFeedbackOptions: HapticOptions = { @@ -26,10 +26,11 @@ const hapticFeedbackOptions: HapticOptions = { ignoreAndroidSystemSettings: false, }; -interface Item extends UploadItem { +export interface Item extends UploadItem { progress?: number; failed?: boolean; // true on timeout or error - completed?: boolean; // true when request is done + completedAt?: number; // when request is done + startedAt?: number; // when request starts } export default function App() { @@ -42,20 +43,17 @@ export default function App() { // optional below method: 'POST', timeout: 60000, // you can set this lower to cause timeouts to happen - onProgress: ({ item, event }) => { - const progress = event?.loaded - ? Math.round((event.loaded / event.total) * 100) - : 0; - updateItem({ - item, - keysAndValues: [{ key: 'progress', value: progress }], - }); - }, + onProgress, onDone: (_data) => { //console.log('onDone, data: ', data); updateItem({ item: _data.item, - keysAndValues: [{ key: 'completed', value: true }], + keysAndValues: [ + { + key: 'completedAt', + value: new Date().getTime(), + }, + ], }); }, onError: (_data) => { @@ -111,6 +109,43 @@ export default function App() { }); }; + async function onProgress({ + item, + event, + }: { + item: Item; + event: OnProgressData['event']; + }) { + const progress = event?.loaded + ? Math.round((event.loaded / event.total) * 100) + : 0; + + // This logic before the else below is a hack to + // simulate progress for ones that upload immediately. + // This is needed after moving to FastImage?!?! + const now = new Date().getTime(); + const elapsed = now - item.startedAt!; + if (progress >= 100 && elapsed <= 200) { + for (let i = 0; i <= 100; i += 25) { + updateItem({ + item, + keysAndValues: [ + { + key: 'progress', + value: i, + }, + ], + }); + await sleep(800); + } + } else { + updateItem({ + item, + keysAndValues: [{ key: 'progress', value: progress }], + }); + } + } + const onPressSelectMedia = async () => { const response = await launchImageLibrary({ mediaType: 'photo', @@ -127,16 +162,9 @@ export default function App() { setData((prevState) => [...prevState, ...items]); }; - const onPressUpload = async () => { - // allow uploading any that previously failed - setData((prevState) => - [...prevState].map((item) => ({ - ...item, - failed: false, - })) - ); - - const promises = data + // :~) + const putItOnTheLine = async (_data: Item[]) => { + const promises = _data .filter((item) => typeof item.progress !== 'number') // leave out any in progress .map((item) => startUpload(item)); // use Promise.all here if you want an error from a timeout or error @@ -144,6 +172,21 @@ export default function App() { console.log('result: ', result); }; + const onPressUpload = async () => { + // allow uploading any that previously failed + setData((prevState) => { + const newState = [...prevState].map((item) => ({ + ...item, + failed: false, + startedAt: new Date().getTime(), + })); + + putItOnTheLine(newState); + + return newState; + }); + }; + const onPressDeleteItem = (item: Item) => () => { setData((prevState) => { const newState = [...prevState]; @@ -166,6 +209,10 @@ export default function App() { key: 'failed', value: false, }, + { + key: 'startedAt', + value: new Date().getTime(), + }, ], }); // wrapped in try/catch here just to get rid of possible unhandled promise warning @@ -230,7 +277,9 @@ export default function App() { ) : null} - {item.completed ? : null} + {item.completedAt ? ( + + ) : null} diff --git a/example/src/util/allSettled.ts b/example/src/util/general.ts similarity index 73% rename from example/src/util/allSettled.ts rename to example/src/util/general.ts index e4f736c..0ddbb3e 100644 --- a/example/src/util/allSettled.ts +++ b/example/src/util/general.ts @@ -1,3 +1,6 @@ +export const sleep = (time: number) => + new Promise((resolve) => setTimeout(resolve, time)); + export const allSettled = (promises: Promise[]) => { return Promise.all( promises.map((promise) => From 66c5797af4d58a2775a8837dac9c1f7f0e1b6c62 Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Tue, 8 Nov 2022 21:51:06 -0600 Subject: [PATCH 7/8] Bump package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4592f66..feb3031 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-use-file-upload", - "version": "0.1.1", + "version": "0.1.2", "description": "A hook for uploading files using multipart form data with React Native. Provides a simple way to track upload progress, abort an upload, and handle timeouts. Written in TypeScript and no dependencies required.", "main": "lib/commonjs/index", "module": "lib/module/index", From 30831c05cc866fc05ac58b51db65c52707399ceb Mon Sep 17 00:00:00 2001 From: Ross Martin <2498502+rossmartin@users.noreply.github.com> Date: Tue, 8 Nov 2022 21:55:49 -0600 Subject: [PATCH 8/8] Update comment --- example/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 5d3f42b..6bed716 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -26,7 +26,7 @@ const hapticFeedbackOptions: HapticOptions = { ignoreAndroidSystemSettings: false, }; -export interface Item extends UploadItem { +interface Item extends UploadItem { progress?: number; failed?: boolean; // true on timeout or error completedAt?: number; // when request is done @@ -121,7 +121,7 @@ export default function App() { : 0; // This logic before the else below is a hack to - // simulate progress for ones that upload immediately. + // simulate progress for any that upload immediately. // This is needed after moving to FastImage?!?! const now = new Date().getTime(); const elapsed = now - item.startedAt!;