From b41675c763fe3da69c594a06648e5097ea6f3c6b Mon Sep 17 00:00:00 2001 From: hailey Date: Tue, 29 Apr 2025 21:47:30 -0700 Subject: [PATCH] [new arch] remove rn-fetch-blob dependency (#8294) * remove fetchblob dependency * give error a name * fix timeout * give qr code dialog a handle * fix error toast text * more bumps that let the thing build on ios * bumps that let it build for ios * [new arch] experiment - use animatedview/animatedref to measure rect for lightbox (#8298) * more upgrades * use animatedview/animatedref for measurements in lightbox * rm usehandleref * downgrade dynamic app icon * more bumps that let the thing build on ios * bumps that let it build for ios * bump expo --------- Co-authored-by: Samuel Newman --------- Co-authored-by: Samuel Newman --- __tests__/lib/images.test.ts | 51 ++++++--------------- jest/jestSetup.js | 9 +--- package.json | 1 - src/lib/media/manip.ts | 87 +++++++++++++++--------------------- yarn.lock | 29 +----------- 5 files changed, 55 insertions(+), 122 deletions(-) diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts index a5acad25f6..c7a645d39c 100644 --- a/__tests__/lib/images.test.ts +++ b/__tests__/lib/images.test.ts @@ -1,10 +1,9 @@ -import {deleteAsync} from 'expo-file-system' +import {createDownloadResumable, deleteAsync} from 'expo-file-system' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' -import RNFetchBlob from 'rn-fetch-blob' import { downloadAndResize, - DownloadAndResizeOpts, + type DownloadAndResizeOpts, getResizedDimensions, } from '../../src/lib/media/manip' @@ -32,11 +31,12 @@ describe('downloadAndResize', () => { }) it('should return resized image for valid URI and options', async () => { - const mockedFetch = RNFetchBlob.fetch as jest.Mock - mockedFetch.mockResolvedValueOnce({ - path: jest.fn().mockReturnValue('file://downloaded-image.jpg'), - info: jest.fn().mockReturnValue({status: 200}), - flush: jest.fn(), + const mockedFetch = createDownloadResumable as jest.Mock + mockedFetch.mockReturnValue({ + cancelAsync: jest.fn(), + downloadAsync: jest + .fn() + .mockResolvedValue({uri: 'file://resized-image.jpg'}), }) const opts: DownloadAndResizeOpts = { @@ -50,13 +50,12 @@ describe('downloadAndResize', () => { const result = await downloadAndResize(opts) expect(result).toEqual(mockResizedImage) - expect(RNFetchBlob.config).toHaveBeenCalledWith({ - fileCache: true, - appendExt: 'jpeg', - }) - expect(RNFetchBlob.fetch).toHaveBeenCalledWith( - 'GET', - 'https://example.com/image.jpg', + expect(createDownloadResumable).toHaveBeenCalledWith( + opts.uri, + expect.anything(), + { + cache: true, + }, ) // First time it gets called is to get dimensions @@ -86,28 +85,6 @@ describe('downloadAndResize', () => { expect(result).toBeUndefined() }) - it('should return undefined for non-200 response', async () => { - const mockedFetch = RNFetchBlob.fetch as jest.Mock - mockedFetch.mockResolvedValueOnce({ - path: jest.fn().mockReturnValue('file://downloaded-image'), - info: jest.fn().mockReturnValue({status: 400}), - flush: jest.fn(), - }) - - const opts: DownloadAndResizeOpts = { - uri: 'https://example.com/image', - width: 100, - height: 100, - maxSize: 500000, - mode: 'cover', - timeout: 10000, - } - - const result = await downloadAndResize(opts) - expect(errorSpy).not.toHaveBeenCalled() - expect(result).toBeUndefined() - }) - it('should not downsize whenever dimensions are below the max dimensions', () => { const initialDimensionsOne = { width: 1200, diff --git a/jest/jestSetup.js b/jest/jestSetup.js index d303225f6c..700d20afc0 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -33,15 +33,10 @@ jest.mock('react-native-safe-area-context', () => { } }) -jest.mock('rn-fetch-blob', () => ({ - config: jest.fn().mockReturnThis(), - cancel: jest.fn(), - fetch: jest.fn(), -})) - jest.mock('expo-file-system', () => ({ getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}), deleteAsync: jest.fn(), + createDownloadResumable: jest.fn(), })) jest.mock('expo-image-manipulator', () => ({ @@ -101,7 +96,7 @@ jest.mock('expo-modules-core', () => ({ } } }), - requireNativeViewManager: jest.fn().mockImplementation(moduleName => { + requireNativeViewManager: jest.fn().mockImplementation(_ => { return () => null }), })) diff --git a/package.json b/package.json index 7c9b8a3e15..6797a43825 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,6 @@ "react-remove-scroll-bar": "^2.3.8", "react-responsive": "^9.0.2", "react-textarea-autosize": "^8.5.3", - "rn-fetch-blob": "^0.12.0", "statsig-react-native-expo": "^4.6.1", "tippy.js": "^6.3.7", "tlds": "^1.234.0", diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index 62cbc55ac5..c7a429a243 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -1,8 +1,9 @@ -import {Image as RNImage, Share as RNShare} from 'react-native' +import {Image as RNImage} from 'react-native' import uuid from 'react-native-uuid' import { cacheDirectory, copyAsync, + createDownloadResumable, deleteAsync, EncodingType, getInfoAsync, @@ -14,7 +15,6 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import * as MediaLibrary from 'expo-media-library' import * as Sharing from 'expo-sharing' import {Buffer} from 'buffer' -import RNFetchBlob from 'rn-fetch-blob' import {POST_IMG_MAX} from '#/lib/constants' import {logger} from '#/logger' @@ -68,28 +68,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) { return } - let downloadRes + const path = createPath(appendExt) + try { - const downloadResPromise = RNFetchBlob.config({ - fileCache: true, - appendExt, - }).fetch('GET', opts.uri) - const to1 = setTimeout(() => downloadResPromise.cancel(), opts.timeout) - downloadRes = await downloadResPromise - clearTimeout(to1) - - const status = downloadRes.info().status - if (status !== 200) { - return - } - - const localUri = normalizePath(downloadRes.path(), true) - return await doResize(localUri, opts) + await downloadImage(opts.uri, path, opts.timeout) + return await doResize(path, opts) } finally { - // TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey - if (downloadRes) { - downloadRes.flush() - } + safeDeleteAsync(path) } } @@ -98,32 +83,16 @@ export async function shareImageModal({uri}: {uri: string}) { // TODO might need to give an error to the user in this case -prf return } - const downloadResponse = await RNFetchBlob.config({ - fileCache: true, - }).fetch('GET', uri) - // NOTE - // assuming PNG // we're currently relying on the fact our CDN only serves pngs // -prf - - let imagePath = downloadResponse.path() - imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true) - - // NOTE - // for some reason expo-sharing refuses to work on iOS - // ...and visa versa - // -prf - if (isIOS) { - await RNShare.share({url: imagePath}) - } else { - await Sharing.shareAsync(imagePath, { - mimeType: 'image/png', - UTI: 'image/png', - }) - } - - safeDeleteAsync(imagePath) + const imageUri = await downloadImage(uri, createPath('png'), 5e3) + const imagePath = await moveToPermanentPath(imageUri, '.png') + safeDeleteAsync(imageUri) + await Sharing.shareAsync(imagePath, { + mimeType: 'image/png', + UTI: 'image/png', + }) } const ALBUM_NAME = 'Bluesky' @@ -134,11 +103,8 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) { // assuming PNG // we're currently relying on the fact our CDN only serves pngs // -prf - const downloadResponse = await RNFetchBlob.config({ - fileCache: true, - }).fetch('GET', uri) - let imagePath = downloadResponse.path() - imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true) + const imageUri = await downloadImage(uri, createPath('png'), 5e3) + const imagePath = await moveToPermanentPath(imageUri, '.png') // save try { @@ -403,3 +369,24 @@ export function getResizedDimensions(originalDims: { height: Math.round(originalDims.height * ratio), } } + +function createPath(ext: string) { + // cacheDirectory will never be null on native, so the null check here is not necessary except for typescript. + // we use a web-only function for downloadAndResize on web + return `${cacheDirectory ?? ''}/${uuid.v4()}.${ext}` +} + +async function downloadImage(uri: string, path: string, timeout: number) { + const dlResumable = createDownloadResumable(uri, path, {cache: true}) + + const to1 = setTimeout(() => dlResumable.cancelAsync(), timeout) + + const dlRes = await dlResumable.downloadAsync() + clearTimeout(to1) + + if (!dlRes?.uri) { + throw new Error('Failed to download image - dlRes is undefined') + } + + return normalizePath(dlRes.uri) +} diff --git a/yarn.lock b/yarn.lock index 8a69afdb35..50a494d500 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6184,7 +6184,7 @@ "@babel/traverse" "^7.25.3" "@react-native/codegen" "0.79.2" -"@react-native/babel-preset@0.79.0-rc.4", "@react-native/babel-preset@0.79.2": +"@react-native/babel-preset@0.79.2": version "0.79.2" resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.79.2.tgz#5a683a6efeea357a326f70c84a881be2bafbeae3" integrity sha512-/HNu869oUq4FUXizpiNWrIhucsYZqu0/0spudJEzk9SEKar0EjVDP7zkg/sKK+KccNypDQGW7nFXT8onzvQ3og== @@ -8779,11 +8779,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-64@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" - integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== - base64-arraybuffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" @@ -12109,18 +12104,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" - integrity sha512-f8c0rE8JiCxpa52kWPAOa3ZaYEnzofDzCQLCn3Vdk0Z5OVLq3BsRFJI4S4ykpeVW6QMGBUkMeUpoEgWnMTnw5Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^10.3.10: version "10.3.12" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" @@ -14876,7 +14859,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -17571,14 +17554,6 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rn-fetch-blob@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba" - integrity sha512-+QnR7AsJ14zqpVVUbzbtAjq0iI8c9tCg49tIoKO2ezjzRunN7YL6zFSFSWZm6d+mE/l9r+OeDM3jmb2tBb2WbA== - dependencies: - base-64 "0.1.0" - glob "7.0.6" - roarr@^7.0.4: version "7.15.1" resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.1.tgz#e4d93105c37b5ea7dd1200d96a3500f757ddc39f"