[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:
@@ -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,
|
||||
|
||||
+2
-7
@@ -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
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -211,7 +211,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",
|
||||
|
||||
@@ -4,7 +4,7 @@ import type ViewShot from 'react-native-view-shot'
|
||||
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||
import {createAssetAsync} from 'expo-media-library'
|
||||
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 {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -15,7 +15,7 @@ import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {QrCode} from '#/components/StarterPack/QrCode'
|
||||
import * as bsky from '#/types/bsky'
|
||||
@@ -155,6 +155,7 @@ export function QrCodeDialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Create a QR code for a starter pack`)}>
|
||||
<View style={[a.flex_1, a.align_center, a.gap_5xl]}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||
import {AppBskyGraphDefs} from '@atproto/api'
|
||||
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -15,7 +15,7 @@ import {isNative, isWeb} from '#/platform/detection'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -76,8 +76,8 @@ function ShareDialogInner({
|
||||
Toast.show(_(msg`Image saved to your camera roll!`))
|
||||
control.close()
|
||||
} catch (e: unknown) {
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark')
|
||||
logger.error('Failed to save QR code', {error: e})
|
||||
Toast.show(_(msg`An error occurred while saving the image!`), 'xmark')
|
||||
logger.error('Failed to save starter pack image', {error: e})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+41
-56
@@ -1,9 +1,10 @@
|
||||
import {Image as RNImage, Share as RNShare} from 'react-native'
|
||||
import {Image} from 'react-native-image-crop-picker'
|
||||
import {Image as RNImage} from 'react-native'
|
||||
import {type Image} from 'react-native-image-crop-picker'
|
||||
import uuid from 'react-native-uuid'
|
||||
import {
|
||||
cacheDirectory,
|
||||
copyAsync,
|
||||
createDownloadResumable,
|
||||
deleteAsync,
|
||||
EncodingType,
|
||||
getInfoAsync,
|
||||
@@ -15,12 +16,11 @@ 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'
|
||||
import {isAndroid, isIOS} from '#/platform/detection'
|
||||
import {Dimensions} from './types'
|
||||
import {type Dimensions} from './types'
|
||||
|
||||
export async function compressIfNeeded(
|
||||
img: Image,
|
||||
@@ -69,28 +69,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||
@@ -133,15 +102,10 @@ 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)
|
||||
|
||||
// save
|
||||
await MediaLibrary.createAssetAsync(imagePath)
|
||||
safeDeleteAsync(imagePath)
|
||||
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||
safeDeleteAsync(imageUri)
|
||||
MediaLibrary.createAssetAsync(imagePath)
|
||||
}
|
||||
|
||||
export function getImageDim(path: string): Promise<Dimensions> {
|
||||
@@ -366,3 +330,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)
|
||||
}
|
||||
|
||||
@@ -8760,11 +8760,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"
|
||||
@@ -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"
|
||||
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"
|
||||
@@ -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"
|
||||
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==
|
||||
@@ -17552,14 +17535,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"
|
||||
|
||||
Reference in New Issue
Block a user