[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 <mozzius@protonmail.com>

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
hailey
2025-04-29 21:47:30 -07:00
committed by Samuel Newman
parent 185dcb0bff
commit 00a8e12cee
7 changed files with 65 additions and 133 deletions
+14 -37
View File
@@ -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 {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import RNFetchBlob from 'rn-fetch-blob'
import { import {
downloadAndResize, downloadAndResize,
DownloadAndResizeOpts, type DownloadAndResizeOpts,
getResizedDimensions, getResizedDimensions,
} from '../../src/lib/media/manip' } from '../../src/lib/media/manip'
@@ -32,11 +31,12 @@ describe('downloadAndResize', () => {
}) })
it('should return resized image for valid URI and options', async () => { it('should return resized image for valid URI and options', async () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock const mockedFetch = createDownloadResumable as jest.Mock
mockedFetch.mockResolvedValueOnce({ mockedFetch.mockReturnValue({
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'), cancelAsync: jest.fn(),
info: jest.fn().mockReturnValue({status: 200}), downloadAsync: jest
flush: jest.fn(), .fn()
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
}) })
const opts: DownloadAndResizeOpts = { const opts: DownloadAndResizeOpts = {
@@ -50,13 +50,12 @@ describe('downloadAndResize', () => {
const result = await downloadAndResize(opts) const result = await downloadAndResize(opts)
expect(result).toEqual(mockResizedImage) expect(result).toEqual(mockResizedImage)
expect(RNFetchBlob.config).toHaveBeenCalledWith({ expect(createDownloadResumable).toHaveBeenCalledWith(
fileCache: true, opts.uri,
appendExt: 'jpeg', expect.anything(),
}) {
expect(RNFetchBlob.fetch).toHaveBeenCalledWith( cache: true,
'GET', },
'https://example.com/image.jpg',
) )
// First time it gets called is to get dimensions // First time it gets called is to get dimensions
@@ -86,28 +85,6 @@ describe('downloadAndResize', () => {
expect(result).toBeUndefined() 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', () => { it('should not downsize whenever dimensions are below the max dimensions', () => {
const initialDimensionsOne = { const initialDimensionsOne = {
width: 1200, width: 1200,
+2 -7
View File
@@ -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', () => ({ jest.mock('expo-file-system', () => ({
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}), getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
deleteAsync: jest.fn(), deleteAsync: jest.fn(),
createDownloadResumable: jest.fn(),
})) }))
jest.mock('expo-image-manipulator', () => ({ 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 return () => null
}), }),
})) }))
-1
View File
@@ -211,7 +211,6 @@
"react-remove-scroll-bar": "^2.3.8", "react-remove-scroll-bar": "^2.3.8",
"react-responsive": "^9.0.2", "react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.5.3",
"rn-fetch-blob": "^0.12.0",
"statsig-react-native-expo": "^4.6.1", "statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tlds": "^1.234.0", "tlds": "^1.234.0",
+3 -2
View File
@@ -4,7 +4,7 @@ import type ViewShot from 'react-native-view-shot'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
import {createAssetAsync} from 'expo-media-library' import {createAssetAsync} from 'expo-media-library'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -15,7 +15,7 @@ import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog' import {type DialogControlProps} from '#/components/Dialog'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {QrCode} from '#/components/StarterPack/QrCode' import {QrCode} from '#/components/StarterPack/QrCode'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
@@ -155,6 +155,7 @@ export function QrCodeDialog({
return ( return (
<Dialog.Outer control={control}> <Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner <Dialog.ScrollableInner
label={_(msg`Create a QR code for a starter pack`)}> label={_(msg`Create a QR code for a starter pack`)}>
<View style={[a.flex_1, a.align_center, a.gap_5xl]}> <View style={[a.flex_1, a.align_center, a.gap_5xl]}>
+4 -4
View File
@@ -1,7 +1,7 @@
import {View} from 'react-native' import {View} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
import {AppBskyGraphDefs} from '@atproto/api' import {type AppBskyGraphDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -15,7 +15,7 @@ import {isNative, isWeb} from '#/platform/detection'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {DialogControlProps} from '#/components/Dialog' import {type DialogControlProps} from '#/components/Dialog'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -76,8 +76,8 @@ function ShareDialogInner({
Toast.show(_(msg`Image saved to your camera roll!`)) Toast.show(_(msg`Image saved to your camera roll!`))
control.close() control.close()
} catch (e: unknown) { } catch (e: unknown) {
Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark') Toast.show(_(msg`An error occurred while saving the image!`), 'xmark')
logger.error('Failed to save QR code', {error: e}) logger.error('Failed to save starter pack image', {error: e})
return return
} }
} }
+41 -56
View File
@@ -1,9 +1,10 @@
import {Image as RNImage, Share as RNShare} from 'react-native' import {Image as RNImage} from 'react-native'
import {Image} from 'react-native-image-crop-picker' import {type Image} from 'react-native-image-crop-picker'
import uuid from 'react-native-uuid' import uuid from 'react-native-uuid'
import { import {
cacheDirectory, cacheDirectory,
copyAsync, copyAsync,
createDownloadResumable,
deleteAsync, deleteAsync,
EncodingType, EncodingType,
getInfoAsync, getInfoAsync,
@@ -15,12 +16,11 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import {Buffer} from 'buffer' import {Buffer} from 'buffer'
import RNFetchBlob from 'rn-fetch-blob'
import {POST_IMG_MAX} from '#/lib/constants' import {POST_IMG_MAX} from '#/lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isAndroid, isIOS} from '#/platform/detection' import {isAndroid, isIOS} from '#/platform/detection'
import {Dimensions} from './types' import {type Dimensions} from './types'
export async function compressIfNeeded( export async function compressIfNeeded(
img: Image, img: Image,
@@ -69,28 +69,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
return return
} }
let downloadRes const path = createPath(appendExt)
try { try {
const downloadResPromise = RNFetchBlob.config({ await downloadImage(opts.uri, path, opts.timeout)
fileCache: true, return await doResize(path, opts)
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)
} finally { } finally {
// TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey safeDeleteAsync(path)
if (downloadRes) {
downloadRes.flush()
}
} }
} }
@@ -99,32 +84,16 @@ export async function shareImageModal({uri}: {uri: string}) {
// TODO might need to give an error to the user in this case -prf // TODO might need to give an error to the user in this case -prf
return 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 // we're currently relying on the fact our CDN only serves pngs
// -prf // -prf
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
let imagePath = downloadResponse.path() const imagePath = await moveToPermanentPath(imageUri, '.png')
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true) safeDeleteAsync(imageUri)
await Sharing.shareAsync(imagePath, {
// NOTE mimeType: 'image/png',
// for some reason expo-sharing refuses to work on iOS UTI: 'image/png',
// ...and visa versa })
// -prf
if (isIOS) {
await RNShare.share({url: imagePath})
} else {
await Sharing.shareAsync(imagePath, {
mimeType: 'image/png',
UTI: 'image/png',
})
}
safeDeleteAsync(imagePath)
} }
export async function saveImageToMediaLibrary({uri}: {uri: string}) { export async function saveImageToMediaLibrary({uri}: {uri: string}) {
@@ -133,15 +102,10 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
// assuming PNG // assuming PNG
// we're currently relying on the fact our CDN only serves pngs // we're currently relying on the fact our CDN only serves pngs
// -prf // -prf
const downloadResponse = await RNFetchBlob.config({ const imageUri = await downloadImage(uri, createPath('png'), 5e3)
fileCache: true, const imagePath = await moveToPermanentPath(imageUri, '.png')
}).fetch('GET', uri) safeDeleteAsync(imageUri)
let imagePath = downloadResponse.path() MediaLibrary.createAssetAsync(imagePath)
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
// save
await MediaLibrary.createAssetAsync(imagePath)
safeDeleteAsync(imagePath)
} }
export function getImageDim(path: string): Promise<Dimensions> { export function getImageDim(path: string): Promise<Dimensions> {
@@ -366,3 +330,24 @@ export function getResizedDimensions(originalDims: {
height: Math.round(originalDims.height * ratio), 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)
}
+1 -26
View File
@@ -8760,11 +8760,6 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 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: base64-arraybuffer@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
@@ -12085,18 +12080,6 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== 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: glob@^10.3.10:
version "10.3.12" version "10.3.12"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b"
@@ -14857,7 +14840,7 @@ minimalistic-crypto-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== 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" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -17552,14 +17535,6 @@ rimraf@^3.0.2:
dependencies: dependencies:
glob "^7.1.3" 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: roarr@^7.0.4:
version "7.15.1" version "7.15.1"
resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.1.tgz#e4d93105c37b5ea7dd1200d96a3500f757ddc39f" resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.1.tgz#e4d93105c37b5ea7dd1200d96a3500f757ddc39f"