diff options
author | hailey <me@haileyok.com> | 2025-05-06 10:54:08 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-06 10:54:08 -0700 |
commit | 521ec8e044e58633530e1864e7abc6e22554d7d3 (patch) | |
tree | e57139a4cfcb9f8859f5e1af008740fc3e8306e3 | |
parent | 973538d246a3f76550611e438152f1a6cad75f49 (diff) | |
download | voidsky-521ec8e044e58633530e1864e7abc6e22554d7d3.tar.zst |
swap out cropper library (#8327)
* mostly implement * type errors * unused import * rm comment * stop accidentally deleting the image while compressing * upgrade * type fixes * upgrade, remove timeout * bump * rm mock * bump --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
-rw-r--r-- | __mocks__/react-native-image-crop-picker.js | 9 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | patches/react-native-image-crop-picker+0.42.0.patch | 48 | ||||
-rw-r--r-- | src/lib/media/manip.ts | 25 | ||||
-rw-r--r-- | src/lib/media/manip.web.ts | 14 | ||||
-rw-r--r-- | src/lib/media/picker.e2e.tsx | 17 | ||||
-rw-r--r-- | src/lib/media/picker.shared.ts | 11 | ||||
-rw-r--r-- | src/lib/media/picker.tsx | 48 | ||||
-rw-r--r-- | src/lib/media/picker.web.tsx | 26 | ||||
-rw-r--r-- | src/lib/media/types.ts | 7 | ||||
-rw-r--r-- | src/screens/Onboarding/StepProfile/index.tsx | 8 | ||||
-rw-r--r-- | src/screens/Profile/Header/EditProfileDialog.tsx | 10 | ||||
-rw-r--r-- | src/state/gallery.ts | 16 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 4 | ||||
-rw-r--r-- | src/state/queries/list.ts | 22 | ||||
-rw-r--r-- | src/state/queries/profile.ts | 6 | ||||
-rw-r--r-- | src/view/com/composer/photos/OpenCameraBtn.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/CreateOrEditList.tsx | 6 | ||||
-rw-r--r-- | src/view/com/modals/CropImage.web.tsx | 6 | ||||
-rw-r--r-- | src/view/com/modals/EditProfile.tsx | 10 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 31 | ||||
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 20 | ||||
-rw-r--r-- | yarn.lock | 10 |
23 files changed, 145 insertions, 215 deletions
diff --git a/__mocks__/react-native-image-crop-picker.js b/__mocks__/react-native-image-crop-picker.js deleted file mode 100644 index 9f901dfac..000000000 --- a/__mocks__/react-native-image-crop-picker.js +++ /dev/null @@ -1,9 +0,0 @@ -export const openPicker = jest - .fn() - .mockImplementation(() => Promise.resolve({uri: ''})) -export const openCamera = jest - .fn() - .mockImplementation(() => Promise.resolve({uri: ''})) -export const openCropper = jest - .fn() - .mockImplementation(() => Promise.resolve({uri: ''})) diff --git a/package.json b/package.json index 5d308958e..5a3a80186 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "expo-font": "~13.3.0", "expo-haptics": "~14.1.4", "expo-image": "~2.1.6", + "expo-image-crop-tool": "^0.1.7", "expo-image-manipulator": "~13.1.5", "expo-image-picker": "~16.1.4", "expo-linear-gradient": "~14.1.4", @@ -188,7 +189,6 @@ "react-native-edge-to-edge": "^1.6.0", "react-native-gesture-handler": "2.25.0", "react-native-get-random-values": "~1.11.0", - "react-native-image-crop-picker": "^0.42.0", "react-native-ios-context-menu": "^1.15.3", "react-native-keyboard-controller": "^1.17.1", "react-native-mmkv": "^2.12.2", diff --git a/patches/react-native-image-crop-picker+0.42.0.patch b/patches/react-native-image-crop-picker+0.42.0.patch deleted file mode 100644 index c6124ad1e..000000000 --- a/patches/react-native-image-crop-picker+0.42.0.patch +++ /dev/null @@ -1,48 +0,0 @@ -diff --git a/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml b/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml -index a08629b..fab6299 100644 ---- a/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml -+++ b/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml -@@ -24,7 +24,7 @@ - - <activity - android:name="com.yalantis.ucrop.UCropActivity" -- android:theme="@style/Theme.AppCompat.Light.NoActionBar" /> -+ android:theme="@style/Theme.UCropNoEdgeToEdge" /> - - - <!-- Prompt Google Play services to install the backported photo picker module --> -diff --git a/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml b/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml -new file mode 100644 -index 0000000..5301f74 ---- /dev/null -+++ b/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml -@@ -0,0 +1,5 @@ -+<resources> -+ <style name="Theme.UCropNoEdgeToEdge" parent="Theme.AppCompat.Light.NoActionBar"> -+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> -+ </style> -+</resources> -\ No newline at end of file -diff --git a/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml b/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml -new file mode 100644 -index 0000000..55569aa ---- /dev/null -+++ b/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml -@@ -0,0 +1,3 @@ -+<resources> -+ <style name="Theme.UCropNoEdgeToEdge" parent="Theme.AppCompat.Light.NoActionBar"/> -+</resources> -\ No newline at end of file -diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m -index 9f20973..c414a7a 100644 ---- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m -+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m -@@ -126,7 +126,7 @@ - (void) setConfiguration:(NSDictionary *)options - - - (UIViewController*) getRootVC { - UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; -- while (root.presentedViewController != nil) { -+ while (root.presentedViewController != nil && !root.presentedViewController.isBeingDismissed) { - root = root.presentedViewController; - } - diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index f6ef8347d..ff5b71ace 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -1,5 +1,4 @@ import {Image as RNImage, Share as RNShare} from 'react-native' -import {Image} from 'react-native-image-crop-picker' import uuid from 'react-native-uuid' import { cacheDirectory, @@ -20,17 +19,17 @@ 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 PickerImage} from './picker.shared' +import {type Dimensions} from './types' export async function compressIfNeeded( - img: Image, + img: PickerImage, maxSize: number = 1000000, -): Promise<Image> { - const origUri = `file://${img.path}` +): Promise<PickerImage> { if (img.size < maxSize) { return img } - const resizedImage = await doResize(origUri, { + const resizedImage = await doResize(normalizePath(img.path), { width: img.width, height: img.height, mode: 'stretch', @@ -166,7 +165,10 @@ interface DoResizeOpts { maxSize: number } -async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> { +async function doResize( + localUri: string, + opts: DoResizeOpts, +): Promise<PickerImage> { // We need to get the dimensions of the image before we resize it. Previously, the library we used allowed us to enter // a "max size", and it would do the "best possible size" calculation for us. // Now instead, we have to supply the final dimensions to the manipulation function instead. @@ -181,6 +183,7 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> { let minQualityPercentage = 0 let maxQualityPercentage = 101 // exclusive let newDataUri + const intermediateUris = [] while (maxQualityPercentage - minQualityPercentage > 1) { const qualityPercentage = Math.round( @@ -195,6 +198,8 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> { }, ) + intermediateUris.push(resizeRes.uri) + const fileInfo = await getInfoAsync(resizeRes.uri) if (!fileInfo.exists) { throw new Error( @@ -214,8 +219,12 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> { } else { maxQualityPercentage = qualityPercentage } + } - safeDeleteAsync(resizeRes.uri) + for (const intermediateUri of intermediateUris) { + if (newDataUri?.path !== normalizePath(intermediateUri)) { + safeDeleteAsync(intermediateUri) + } } if (newDataUri) { diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts index ffef7314d..ffcf0c533 100644 --- a/src/lib/media/manip.web.ts +++ b/src/lib/media/manip.web.ts @@ -1,12 +1,11 @@ -import {Image as RNImage} from 'react-native-image-crop-picker' - -import {Dimensions} from './types' +import {type PickerImage} from './picker.shared' +import {type Dimensions} from './types' import {blobToDataUri, getDataUriSize} from './util' export async function compressIfNeeded( - img: RNImage, + img: PickerImage, maxSize: number, -): Promise<RNImage> { +): Promise<PickerImage> { if (img.size < maxSize) { return img } @@ -69,7 +68,10 @@ interface DoResizeOpts { maxSize: number } -async function doResize(dataUri: string, opts: DoResizeOpts): Promise<RNImage> { +async function doResize( + dataUri: string, + opts: DoResizeOpts, +): Promise<PickerImage> { let newDataUri let minQualityPercentage = 0 diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx index fc6fcde45..a2a9357ec 100644 --- a/src/lib/media/picker.e2e.tsx +++ b/src/lib/media/picker.e2e.tsx @@ -1,15 +1,12 @@ import { - Image as RNImage, - openCropper as openCropperFn, -} from 'react-native-image-crop-picker' -import { documentDirectory, getInfoAsync, readDirectoryAsync, } from 'expo-file-system' +import ExpoImageCropTool, {type OpenCropperOptions} from 'expo-image-crop-tool' import {compressIfNeeded} from './manip' -import {CropperOptions} from './types' +import {type PickerImage} from './picker.shared' async function getFile() { const imagesDir = documentDirectory! @@ -37,18 +34,18 @@ async function getFile() { }) } -export async function openPicker(): Promise<RNImage[]> { +export async function openPicker(): Promise<PickerImage[]> { return [await getFile()] } -export async function openCamera(): Promise<RNImage> { +export async function openCamera(): Promise<PickerImage> { return await getFile() } -export async function openCropper(opts: CropperOptions) { - const item = await openCropperFn({ +export async function openCropper(opts: OpenCropperOptions) { + const item = await ExpoImageCropTool.openCropperAsync({ ...opts, - forceJpg: true, // ios only + format: 'jpeg', }) return { diff --git a/src/lib/media/picker.shared.ts b/src/lib/media/picker.shared.ts index a45bf5c0f..21e680832 100644 --- a/src/lib/media/picker.shared.ts +++ b/src/lib/media/picker.shared.ts @@ -1,14 +1,21 @@ import { - ImagePickerOptions, + type ImagePickerOptions, launchImageLibraryAsync, MediaTypeOptions, } from 'expo-image-picker' -// TODO: replace global i18n instance with one returned from useLingui -sfn import {t} from '@lingui/macro' import * as Toast from '#/view/com/util/Toast' import {getDataUriSize} from './util' +export type PickerImage = { + mime: string + height: number + width: number + path: string + size: number +} + export async function openPicker(opts?: ImagePickerOptions) { const response = await launchImageLibraryAsync({ exif: false, diff --git a/src/lib/media/picker.tsx b/src/lib/media/picker.tsx index 37e01e67f..6095730d5 100644 --- a/src/lib/media/picker.tsx +++ b/src/lib/media/picker.tsx @@ -1,36 +1,34 @@ -import { - Image as RNImage, - openCamera as openCameraFn, - openCropper as openCropperFn, -} from 'react-native-image-crop-picker' +import ExpoImageCropTool, {type OpenCropperOptions} from 'expo-image-crop-tool' +import {type ImagePickerOptions, launchCameraAsync} from 'expo-image-picker' -import {CameraOpts, CropperOptions} from './types' -export {openPicker} from './picker.shared' +export {openPicker, type PickerImage as RNImage} from './picker.shared' -export async function openCamera(opts: CameraOpts): Promise<RNImage> { - const item = await openCameraFn({ - width: opts.width, - height: opts.height, - freeStyleCropEnabled: opts.freeStyleCropEnabled, - cropperCircleOverlay: opts.cropperCircleOverlay, - cropping: false, - forceJpg: true, // ios only - compressImageQuality: 0.8, - }) +export async function openCamera(customOpts: ImagePickerOptions) { + const opts: ImagePickerOptions = { + mediaTypes: 'images', + ...customOpts, + } + const res = await launchCameraAsync(opts) + + if (!res || !res.assets) { + throw new Error('Camera was closed before taking a photo') + } + + const asset = res?.assets[0] return { - path: item.path, - mime: item.mime, - size: item.size, - width: item.width, - height: item.height, + path: asset.uri, + mime: asset.mimeType ?? 'image/jpeg', + size: asset.fileSize ?? 0, + width: asset.width, + height: asset.height, } } -export async function openCropper(opts: CropperOptions) { - const item = await openCropperFn({ +export async function openCropper(opts: OpenCropperOptions) { + const item = await ExpoImageCropTool.openCropperAsync({ ...opts, - forceJpg: true, // ios only + format: 'jpeg', }) return { diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx index a53ffc961..b7d0d6f06 100644 --- a/src/lib/media/picker.web.tsx +++ b/src/lib/media/picker.web.tsx @@ -1,29 +1,29 @@ /// <reference lib="dom" /> -import {Image as RNImage} from 'react-native-image-crop-picker' +import {type OpenCropperOptions} from 'expo-image-crop-tool' -import {CameraOpts, CropperOptions} from './types' -export {openPicker} from './picker.shared' import {unstable__openModal} from '#/state/modals' +import {type PickerImage} from './picker.shared' +import {type CameraOpts} from './types' -export async function openCamera(_opts: CameraOpts): Promise<RNImage> { +export {openPicker, type PickerImage as RNImage} from './picker.shared' + +export async function openCamera(_opts: CameraOpts): Promise<PickerImage> { // const mediaType = opts.mediaType || 'photo' TODO throw new Error('TODO') } -export async function openCropper(opts: CropperOptions): Promise<RNImage> { +export async function openCropper( + opts: OpenCropperOptions, +): Promise<PickerImage> { // TODO handle more opts return new Promise((resolve, reject) => { unstable__openModal({ name: 'crop-image', - uri: opts.path, - dimensions: - opts.width && opts.height - ? {width: opts.width, height: opts.height} - : undefined, - aspect: opts.webAspectRatio, - circular: opts.webCircularCrop, - onSelect: (img?: RNImage) => { + uri: opts.imageUri, + aspect: opts.aspectRatio, + circular: opts.shape === 'circle', + onSelect: (img?: PickerImage) => { if (img) { resolve(img) } else { diff --git a/src/lib/media/types.ts b/src/lib/media/types.ts index ec94256ea..c083093ac 100644 --- a/src/lib/media/types.ts +++ b/src/lib/media/types.ts @@ -1,5 +1,3 @@ -import {openCropper} from 'react-native-image-crop-picker' - export interface Dimensions { width: number height: number @@ -17,8 +15,3 @@ export interface CameraOpts { freeStyleCropEnabled?: boolean cropperCircleOverlay?: boolean } - -export type CropperOptions = Parameters<typeof openCropper>[0] & { - webAspectRatio?: number - webCircularCrop?: boolean -} diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 73472ec33..0e738f145 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -182,11 +182,9 @@ export function StepProfile() { if (!isWeb) { image = await openCropper({ - mediaType: 'photo', - cropperCircleOverlay: true, - height: 1000, - width: 1000, - path: image.path, + imageUri: image.path, + shape: 'circle', + aspectRatio: 1 / 1, }) } image = await compressIfNeeded(image, 1000000) diff --git a/src/screens/Profile/Header/EditProfileDialog.tsx b/src/screens/Profile/Header/EditProfileDialog.tsx index a0e24d78a..8a9f0d540 100644 --- a/src/screens/Profile/Header/EditProfileDialog.tsx +++ b/src/screens/Profile/Header/EditProfileDialog.tsx @@ -1,12 +1,12 @@ import {useCallback, useEffect, useState} from 'react' import {Dimensions, View} from 'react-native' -import {type Image as RNImage} from 'react-native-image-crop-picker' import {type AppBskyActorDefs} from '@atproto/api' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {urls} from '#/lib/constants' import {compressIfNeeded} from '#/lib/media/manip' +import {type PickerImage} from '#/lib/media/picker.shared' import {cleanError} from '#/lib/strings/errors' import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers' import {logger} from '#/logger' @@ -127,10 +127,10 @@ function DialogInner({ profile.avatar, ) const [newUserBanner, setNewUserBanner] = useState< - RNImage | undefined | null + PickerImage | undefined | null >() const [newUserAvatar, setNewUserAvatar] = useState< - RNImage | undefined | null + PickerImage | undefined | null >() const dirty = @@ -144,7 +144,7 @@ function DialogInner({ }, [dirty, setDirty]) const onSelectNewAvatar = useCallback( - async (img: RNImage | null) => { + async (img: PickerImage | null) => { setImageError('') if (img === null) { setNewUserAvatar(null) @@ -163,7 +163,7 @@ function DialogInner({ ) const onSelectNewBanner = useCallback( - async (img: RNImage | null) => { + async (img: PickerImage | null) => { setImageError('') if (!img) { setNewUserBanner(null) diff --git a/src/state/gallery.ts b/src/state/gallery.ts index f03ed2afe..73252dde7 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -16,7 +16,7 @@ import {POST_IMG_MAX} from '#/lib/constants' import {getImageDim} from '#/lib/media/manip' import {openCropper} from '#/lib/media/picker' import {getDataUriSize} from '#/lib/media/util' -import {isIOS, isNative} from '#/platform/detection' +import {isNative} from '#/platform/detection' export type ImageTransformation = { crop?: ActionCrop['crop'] @@ -122,25 +122,13 @@ export async function cropImage(img: ComposerImage): Promise<ComposerImage> { return img } - // NOTE - // on ios, react-native-image-crop-picker gives really bad quality - // without specifying width and height. on android, however, the - // crop stretches incorrectly if you do specify it. these are - // both separate bugs in the library. we deal with that by - // providing width & height for ios only - // -prf - const source = img.source - const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX) // @todo: we're always passing the original image here, does image-cropper // allows for setting initial crop dimensions? -mary try { const cropped = await openCropper({ - mediaType: 'photo', - path: source.path, - freeStyleCropEnabled: true, - ...(isIOS ? {width: w, height: h} : {}), + imageUri: source.path, }) return { diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index f79f6213f..3e738898a 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -1,8 +1,8 @@ import React from 'react' -import {type Image as RNImage} from 'react-native-image-crop-picker' import {type AppBskyActorDefs, type AppBskyGraphDefs} from '@atproto/api' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {type PickerImage} from '#/lib/media/picker.shared' export interface EditProfileModal { name: 'edit-profile' @@ -32,7 +32,7 @@ export interface CropImageModal { dimensions?: {width: number; height: number} aspect?: number circular?: boolean - onSelect: (img?: RNImage) => void + onSelect: (img?: PickerImage) => void } export interface DeleteAccountModal { diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts index 260a0bf2c..38e8cd62d 100644 --- a/src/state/queries/list.ts +++ b/src/state/queries/list.ts @@ -1,20 +1,20 @@ -import {Image as RNImage} from 'react-native-image-crop-picker' import { - $Typed, - AppBskyGraphDefs, - AppBskyGraphGetList, - AppBskyGraphList, + type $Typed, + type AppBskyGraphDefs, + type AppBskyGraphGetList, + type AppBskyGraphList, AtUri, - BskyAgent, - ComAtprotoRepoApplyWrites, - Facet, - Un$Typed, + type BskyAgent, + type ComAtprotoRepoApplyWrites, + type Facet, + type Un$Typed, } from '@atproto/api' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import chunk from 'lodash.chunk' import {uploadBlob} from '#/lib/api' import {until} from '#/lib/async/until' +import {type PickerImage} from '#/lib/media/picker.shared' import {STALE} from '#/state/queries' import {useAgent, useSession} from '../session' import {invalidate as invalidateMyLists} from './my-lists' @@ -47,7 +47,7 @@ export interface ListCreateMutateParams { name: string description: string descriptionFacets: Facet[] | undefined - avatar: RNImage | null | undefined + avatar: PickerImage | null | undefined } export function useListCreateMutation() { const {currentAccount} = useSession() @@ -115,7 +115,7 @@ export interface ListMetadataMutateParams { name: string description: string descriptionFacets: Facet[] | undefined - avatar: RNImage | null | undefined + avatar: PickerImage | null | undefined } export function useListMetadataMutation() { const {currentAccount} = useSession() diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 609a62e25..9f40ab7f6 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -1,5 +1,4 @@ import {useCallback} from 'react' -import {type Image as RNImage} from 'react-native-image-crop-picker' import { type AppBskyActorDefs, type AppBskyActorGetProfile, @@ -21,6 +20,7 @@ import { import {uploadBlob} from '#/lib/api' import {until} from '#/lib/async/until' import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' +import {type PickerImage} from '#/lib/media/picker.shared' import {logEvent, type LogEvents, toClout} from '#/lib/statsig/statsig' import {type Shadow} from '#/state/cache/types' import {STALE} from '#/state/queries' @@ -131,8 +131,8 @@ interface ProfileUpdateParams { | (( existing: Un$Typed<AppBskyActorProfile.Record>, ) => Un$Typed<AppBskyActorProfile.Record>) - newUserAvatar?: RNImage | undefined | null - newUserBanner?: RNImage | undefined | null + newUserAvatar?: PickerImage | undefined | null + newUserBanner?: PickerImage | undefined | null checkCommitted?: (res: AppBskyActorGetProfile.Response) => boolean } export function useProfileUpdateMutation() { diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index fb3ab5c8f..1c9440eb1 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -35,9 +35,7 @@ export function OpenCameraBtn({disabled, onAdd}: Props) { } const img = await openCamera({ - width: POST_IMG_MAX.width, - height: POST_IMG_MAX.height, - freeStyleCropEnabled: true, + aspect: [POST_IMG_MAX.width, POST_IMG_MAX.height], }) // If we don't have permissions it's fine, we just wont save it. The post itself will still have access to diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx index 0e4e23b97..a7eae15dd 100644 --- a/src/view/com/modals/CreateOrEditList.tsx +++ b/src/view/com/modals/CreateOrEditList.tsx @@ -8,7 +8,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {type Image as RNImage} from 'react-native-image-crop-picker' import {LinearGradient} from 'expo-linear-gradient' import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' import {msg, Trans} from '@lingui/macro' @@ -17,6 +16,7 @@ import {useLingui} from '@lingui/react' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {compressIfNeeded} from '#/lib/media/manip' +import {type PickerImage} from '#/lib/media/picker.shared' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {enforceLen} from '#/lib/strings/helpers' import {richTextToString} from '#/lib/strings/rich-text-helpers' @@ -95,7 +95,7 @@ export function Component({ const isDescriptionOver = graphemeLength > MAX_DESCRIPTION const [avatar, setAvatar] = useState<string | undefined>(list?.avatar) - const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() + const [newAvatar, setNewAvatar] = useState<PickerImage | undefined | null>() const onDescriptionChange = useCallback( (newText: string) => { @@ -112,7 +112,7 @@ export function Component({ }, [closeModal]) const onSelectNewAvatar = useCallback( - async (img: RNImage | null) => { + async (img: PickerImage | null) => { if (!img) { setNewAvatar(null) setAvatar(undefined) diff --git a/src/view/com/modals/CropImage.web.tsx b/src/view/com/modals/CropImage.web.tsx index 41ca30657..78c0466f0 100644 --- a/src/view/com/modals/CropImage.web.tsx +++ b/src/view/com/modals/CropImage.web.tsx @@ -1,14 +1,14 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {Image as RNImage} from 'react-native-image-crop-picker' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import {LinearGradient} from 'expo-linear-gradient' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import ReactCrop, {PercentCrop} from 'react-image-crop' +import ReactCrop, {type PercentCrop} from 'react-image-crop' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {type PickerImage} from '#/lib/media/picker.shared' import {getDataUriSize} from '#/lib/media/util' import {gradients, s} from '#/lib/styles' import {useModalControls} from '#/state/modals' @@ -25,7 +25,7 @@ export function Component({ uri: string aspect?: number circular?: boolean - onSelect: (img?: RNImage) => void + onSelect: (img?: PickerImage) => void }) { const pal = usePalette('default') const {_} = useLingui() diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index ebc1281a3..cb1552fe5 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -8,7 +8,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {type Image as RNImage} from 'react-native-image-crop-picker' import Animated, {FadeOut} from 'react-native-reanimated' import {LinearGradient} from 'expo-linear-gradient' import {type AppBskyActorDefs} from '@atproto/api' @@ -18,6 +17,7 @@ import {useLingui} from '@lingui/react' import {MAX_DESCRIPTION, MAX_DISPLAY_NAME, urls} from '#/lib/constants' import {usePalette} from '#/lib/hooks/usePalette' import {compressIfNeeded} from '#/lib/media/manip' +import {type PickerImage} from '#/lib/media/picker.shared' import {cleanError} from '#/lib/strings/errors' import {enforceLen} from '#/lib/strings/helpers' import {colors, gradients, s} from '#/lib/styles' @@ -67,16 +67,16 @@ export function Component({ profile.avatar, ) const [newUserBanner, setNewUserBanner] = useState< - RNImage | undefined | null + PickerImage | undefined | null >() const [newUserAvatar, setNewUserAvatar] = useState< - RNImage | undefined | null + PickerImage | undefined | null >() const onPressCancel = () => { closeModal() } const onSelectNewAvatar = useCallback( - async (img: RNImage | null) => { + async (img: PickerImage | null) => { setImageError('') if (img === null) { setNewUserAvatar(null) @@ -95,7 +95,7 @@ export function Component({ ) const onSelectNewBanner = useCallback( - async (img: RNImage | null) => { + async (img: PickerImage | null) => { setImageError('') if (!img) { setNewUserBanner(null) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 20fc1c65d..2450c111b 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -2,14 +2,13 @@ import React, {memo, useMemo} from 'react' import { Image, Pressable, - StyleProp, + type StyleProp, StyleSheet, View, - ViewStyle, + type ViewStyle, } from 'react-native' -import {Image as RNImage} from 'react-native-image-crop-picker' import Svg, {Circle, Path, Rect} from 'react-native-svg' -import {ModerationUI} from '@atproto/api' +import {type ModerationUI} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -38,8 +37,13 @@ import {Link} from '#/components/Link' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import * as Menu from '#/components/Menu' import {ProfileHoverCard} from '#/components/ProfileHoverCard' -import * as bsky from '#/types/bsky' -import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' +import type * as bsky from '#/types/bsky' +import { + openCamera, + openCropper, + openPicker, + type RNImage, +} from '../../../lib/media/picker' export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' @@ -312,9 +316,7 @@ let EditableUserAvatar = ({ onSelectNewAvatar( await openCamera({ - width: 1000, - height: 1000, - cropperCircleOverlay: true, + aspect: [1, 1], }), ) }, [onSelectNewAvatar, requestCameraAccessIfNeeded]) @@ -336,15 +338,10 @@ let EditableUserAvatar = ({ try { const croppedImage = await openCropper({ - mediaType: 'photo', - cropperCircleOverlay: true, - height: 1000, - width: 1000, - path: item.path, - webAspectRatio: 1, - webCircularCrop: true, + imageUri: item.path, + shape: 'circle', + aspectRatio: 1, }) - onSelectNewAvatar(croppedImage) } catch (e: any) { // Don't log errors for cancelling selection to sentry on ios or android diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index e0ace5e48..ab7f25b80 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -1,8 +1,7 @@ import React from 'react' import {Pressable, StyleSheet, View} from 'react-native' -import {Image as RNImage} from 'react-native-image-crop-picker' import {Image} from 'expo-image' -import {ModerationUI} from '@atproto/api' +import {type ModerationUI} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -25,7 +24,12 @@ import { import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import * as Menu from '#/components/Menu' -import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' +import { + openCamera, + openCropper, + openPicker, + type RNImage, +} from '../../../lib/media/picker' export function UserBanner({ type, @@ -52,8 +56,7 @@ export function UserBanner({ } onSelectNewBanner?.( await openCamera({ - width: 3000, - height: 1000, + aspect: [3, 1], }), ) }, [onSelectNewBanner, requestCameraAccessIfNeeded]) @@ -70,11 +73,8 @@ export function UserBanner({ try { onSelectNewBanner?.( await openCropper({ - mediaType: 'photo', - path: items[0].path, - width: 3000, - height: 1000, - webAspectRatio: 3, + imageUri: items[0].path, + aspectRatio: 3 / 1, }), ) } catch (e: any) { diff --git a/yarn.lock b/yarn.lock index cc321e65f..c2533f127 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11247,6 +11247,11 @@ expo-haptics@~14.1.4: resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-14.1.4.tgz#442f48b1bdf83484d4fcadc653445aaae6049b70" integrity sha512-QZdE3NMX74rTuIl82I+n12XGwpDWKb8zfs5EpwsnGi/D/n7O2Jd4tO5ivH+muEG/OCJOMq5aeaVDqqaQOhTkcA== +expo-image-crop-tool@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/expo-image-crop-tool/-/expo-image-crop-tool-0.1.7.tgz#a84ed2192d147d922b3d352e52e29bc3a4c1e800" + integrity sha512-An+tszv0DKHA74Yr7uQb4mqGTxTVBwku9zu8yvhb7HzBXIUGw12hnb8M6ntHZqIFuQiLzBxaKH8DTwZgg9oAnw== + expo-image-loader@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-5.1.0.tgz#f7d65f9b9a9714eaaf5d50a406cb34cb25262153" @@ -16796,11 +16801,6 @@ react-native-get-random-values@~1.11.0: dependencies: fast-base64-decode "^1.0.0" -react-native-image-crop-picker@^0.42.0: - version "0.42.0" - resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.42.0.tgz#0672c080feb8ffefd65ba00a4e64bc8a1066136e" - integrity sha512-EOEkekPJ7g+CNf92HrWAGM4kcDJyVY02gQJUVH7MSNUOK11SHnurXVM0TnwIt410Y4T+lBkq3rfJEA1qDaDDwA== - react-native-ios-context-menu@^1.15.3: version "1.15.3" resolved "https://registry.yarnpkg.com/react-native-ios-context-menu/-/react-native-ios-context-menu-1.15.3.tgz#c02e6a7af2df8c08d0b3e1c8f3395484b3c9c760" |