diff --git a/package.json b/package.json
index 4f7587d5bd..2a854b6780 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.6",
"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": "^3.2.0",
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 c6124ad1e1..0000000000
--- 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 @@
-
-
-+ android:theme="@style/Theme.UCropNoEdgeToEdge" />
-
-
-
-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 @@
-+
-+
-+
-\ 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 @@
-+
-+
-+
-\ 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 b56964596f..911554a10f 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -1,5 +1,4 @@
import {Image as RNImage} from 'react-native'
-import {type Image} from 'react-native-image-crop-picker'
import uuid from 'react-native-uuid'
import {
cacheDirectory,
@@ -20,17 +19,17 @@ import {Buffer} from 'buffer'
import {POST_IMG_MAX} from '#/lib/constants'
import {logger} from '#/logger'
import {isAndroid, isIOS} from '#/platform/detection'
+import {type PickerImage} from './picker.shared'
import {type Dimensions} from './types'
export async function compressIfNeeded(
- img: Image,
+ img: PickerImage,
maxSize: number = 1000000,
-): Promise {
- const origUri = `file://${img.path}`
+): Promise {
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',
@@ -130,7 +129,10 @@ interface DoResizeOpts {
maxSize: number
}
-async function doResize(localUri: string, opts: DoResizeOpts): Promise {
+async function doResize(
+ localUri: string,
+ opts: DoResizeOpts,
+): Promise {
// 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.
@@ -145,6 +147,7 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise {
let minQualityPercentage = 0
let maxQualityPercentage = 101 // exclusive
let newDataUri
+ const intermediateUris = []
while (maxQualityPercentage - minQualityPercentage > 1) {
const qualityPercentage = Math.round(
@@ -159,6 +162,8 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise {
},
)
+ intermediateUris.push(resizeRes.uri)
+
const fileInfo = await getInfoAsync(resizeRes.uri)
if (!fileInfo.exists) {
throw new Error(
@@ -178,8 +183,12 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise {
} 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 ffef7314d7..ffcf0c5335 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 {
+): Promise {
if (img.size < maxSize) {
return img
}
@@ -69,7 +68,10 @@ interface DoResizeOpts {
maxSize: number
}
-async function doResize(dataUri: string, opts: DoResizeOpts): Promise {
+async function doResize(
+ dataUri: string,
+ opts: DoResizeOpts,
+): Promise {
let newDataUri
let minQualityPercentage = 0
diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx
index fc6fcde45e..a2a9357ec2 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 {
+export async function openPicker(): Promise {
return [await getFile()]
}
-export async function openCamera(): Promise {
+export async function openCamera(): Promise {
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 a45bf5c0f1..21e6808324 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 37e01e67f9..6095730d51 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 {
- 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 a53ffc9614..b7d0d6f06d 100644
--- a/src/lib/media/picker.web.tsx
+++ b/src/lib/media/picker.web.tsx
@@ -1,29 +1,29 @@
///
-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 {
+export {openPicker, type PickerImage as RNImage} from './picker.shared'
+
+export async function openCamera(_opts: CameraOpts): Promise {
// const mediaType = opts.mediaType || 'photo' TODO
throw new Error('TODO')
}
-export async function openCropper(opts: CropperOptions): Promise {
+export async function openCropper(
+ opts: OpenCropperOptions,
+): Promise {
// 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 ec94256ea1..c083093ac7 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[0] & {
- webAspectRatio?: number
- webCircularCrop?: boolean
-}
diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx
index 73472ec332..3d2c551e94 100644
--- a/src/screens/Onboarding/StepProfile/index.tsx
+++ b/src/screens/Onboarding/StepProfile/index.tsx
@@ -2,7 +2,7 @@ import React from 'react'
import {View} from 'react-native'
import {Image as ExpoImage} from 'expo-image'
import {
- ImagePickerOptions,
+ type ImagePickerOptions,
launchImageLibraryAsync,
MediaTypeOptions,
} from 'expo-image-picker'
@@ -27,7 +27,7 @@ import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreato
import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
import {
PlaceholderCanvas,
- PlaceholderCanvasRef,
+ type PlaceholderCanvasRef,
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -38,7 +38,7 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive'
import {Text} from '#/components/Typography'
-import {AvatarColor, avatarColors, Emoji, emojiItems} from './types'
+import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
export interface Avatar {
image?: {
@@ -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 a0e24d78a8..8a9f0d5404 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 f03ed2afe8..73252dde74 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 {
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 f79f6213ff..3e738898a3 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 260a0bf2ce..38e8cd62d8 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 609a62e25a..9f40ab7f61 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,
) => Un$Typed)
- 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 fb3ab5c8fc..8bd1aa27b0 100644
--- a/src/view/com/composer/photos/OpenCameraBtn.tsx
+++ b/src/view/com/composer/photos/OpenCameraBtn.tsx
@@ -8,7 +8,7 @@ import {useCameraPermission} from '#/lib/hooks/usePermissions'
import {openCamera} from '#/lib/media/picker'
import {logger} from '#/logger'
import {isMobileWeb, isNative} from '#/platform/detection'
-import {ComposerImage, createComposerImage} from '#/state/gallery'
+import {type ComposerImage, createComposerImage} from '#/state/gallery'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
@@ -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 0e4e23b97f..a7eae15ddb 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(list?.avatar)
- const [newAvatar, setNewAvatar] = useState()
+ const [newAvatar, setNewAvatar] = useState()
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 41ca306573..78c0466f0b 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 ebc1281a33..cb1552fe57 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 20fc1c65da..2450c111b6 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 e0ace5e481..ab7f25b800 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 0ff0baf047..c01b6e116c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11242,6 +11242,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.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/expo-image-crop-tool/-/expo-image-crop-tool-0.1.6.tgz#fd65ea2a143fef9a45a0d0cd2b967ec1f63a0bad"
+ integrity sha512-I0KVaLur+4EBMf6jtp7uVQNS26DgyGWERn2sng6iKc1I2QygILU8jj5/sMqzCB7fGPpzGn7e0S9u6EmR2YJHfg==
+
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"
@@ -16784,11 +16789,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"