An ATproto social media client -- with an independent Appview.

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>

authored by hailey.at

Samuel Newman and committed by
GitHub
521ec8e0 973538d2

+145 -215
-9
__mocks__/react-native-image-crop-picker.js
··· 1 - export const openPicker = jest 2 - .fn() 3 - .mockImplementation(() => Promise.resolve({uri: ''})) 4 - export const openCamera = jest 5 - .fn() 6 - .mockImplementation(() => Promise.resolve({uri: ''})) 7 - export const openCropper = jest 8 - .fn() 9 - .mockImplementation(() => Promise.resolve({uri: ''}))
+1 -1
package.json
··· 142 142 "expo-font": "~13.3.0", 143 143 "expo-haptics": "~14.1.4", 144 144 "expo-image": "~2.1.6", 145 + "expo-image-crop-tool": "^0.1.7", 145 146 "expo-image-manipulator": "~13.1.5", 146 147 "expo-image-picker": "~16.1.4", 147 148 "expo-linear-gradient": "~14.1.4", ··· 188 189 "react-native-edge-to-edge": "^1.6.0", 189 190 "react-native-gesture-handler": "2.25.0", 190 191 "react-native-get-random-values": "~1.11.0", 191 - "react-native-image-crop-picker": "^0.42.0", 192 192 "react-native-ios-context-menu": "^1.15.3", 193 193 "react-native-keyboard-controller": "^1.17.1", 194 194 "react-native-mmkv": "^2.12.2",
-48
patches/react-native-image-crop-picker+0.42.0.patch
··· 1 - 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 2 - index a08629b..fab6299 100644 3 - --- a/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml 4 - +++ b/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml 5 - @@ -24,7 +24,7 @@ 6 - 7 - <activity 8 - android:name="com.yalantis.ucrop.UCropActivity" 9 - - android:theme="@style/Theme.AppCompat.Light.NoActionBar" /> 10 - + android:theme="@style/Theme.UCropNoEdgeToEdge" /> 11 - 12 - 13 - <!-- Prompt Google Play services to install the backported photo picker module --> 14 - 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 15 - new file mode 100644 16 - index 0000000..5301f74 17 - --- /dev/null 18 - +++ b/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml 19 - @@ -0,0 +1,5 @@ 20 - +<resources> 21 - + <style name="Theme.UCropNoEdgeToEdge" parent="Theme.AppCompat.Light.NoActionBar"> 22 - + <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> 23 - + </style> 24 - +</resources> 25 - \ No newline at end of file 26 - 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 27 - new file mode 100644 28 - index 0000000..55569aa 29 - --- /dev/null 30 - +++ b/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml 31 - @@ -0,0 +1,3 @@ 32 - +<resources> 33 - + <style name="Theme.UCropNoEdgeToEdge" parent="Theme.AppCompat.Light.NoActionBar"/> 34 - +</resources> 35 - \ No newline at end of file 36 - 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 37 - index 9f20973..c414a7a 100644 38 - --- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m 39 - +++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m 40 - @@ -126,7 +126,7 @@ - (void) setConfiguration:(NSDictionary *)options 41 - 42 - - (UIViewController*) getRootVC { 43 - UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; 44 - - while (root.presentedViewController != nil) { 45 - + while (root.presentedViewController != nil && !root.presentedViewController.isBeingDismissed) { 46 - root = root.presentedViewController; 47 - } 48 -
+17 -8
src/lib/media/manip.ts
··· 1 1 import {Image as RNImage, Share as RNShare} from 'react-native' 2 - import {Image} from 'react-native-image-crop-picker' 3 2 import uuid from 'react-native-uuid' 4 3 import { 5 4 cacheDirectory, ··· 20 19 import {POST_IMG_MAX} from '#/lib/constants' 21 20 import {logger} from '#/logger' 22 21 import {isAndroid, isIOS} from '#/platform/detection' 23 - import {Dimensions} from './types' 22 + import {type PickerImage} from './picker.shared' 23 + import {type Dimensions} from './types' 24 24 25 25 export async function compressIfNeeded( 26 - img: Image, 26 + img: PickerImage, 27 27 maxSize: number = 1000000, 28 - ): Promise<Image> { 29 - const origUri = `file://${img.path}` 28 + ): Promise<PickerImage> { 30 29 if (img.size < maxSize) { 31 30 return img 32 31 } 33 - const resizedImage = await doResize(origUri, { 32 + const resizedImage = await doResize(normalizePath(img.path), { 34 33 width: img.width, 35 34 height: img.height, 36 35 mode: 'stretch', ··· 166 165 maxSize: number 167 166 } 168 167 169 - async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> { 168 + async function doResize( 169 + localUri: string, 170 + opts: DoResizeOpts, 171 + ): Promise<PickerImage> { 170 172 // We need to get the dimensions of the image before we resize it. Previously, the library we used allowed us to enter 171 173 // a "max size", and it would do the "best possible size" calculation for us. 172 174 // Now instead, we have to supply the final dimensions to the manipulation function instead. ··· 181 183 let minQualityPercentage = 0 182 184 let maxQualityPercentage = 101 // exclusive 183 185 let newDataUri 186 + const intermediateUris = [] 184 187 185 188 while (maxQualityPercentage - minQualityPercentage > 1) { 186 189 const qualityPercentage = Math.round( ··· 195 198 }, 196 199 ) 197 200 201 + intermediateUris.push(resizeRes.uri) 202 + 198 203 const fileInfo = await getInfoAsync(resizeRes.uri) 199 204 if (!fileInfo.exists) { 200 205 throw new Error( ··· 214 219 } else { 215 220 maxQualityPercentage = qualityPercentage 216 221 } 222 + } 217 223 218 - safeDeleteAsync(resizeRes.uri) 224 + for (const intermediateUri of intermediateUris) { 225 + if (newDataUri?.path !== normalizePath(intermediateUri)) { 226 + safeDeleteAsync(intermediateUri) 227 + } 219 228 } 220 229 221 230 if (newDataUri) {
+8 -6
src/lib/media/manip.web.ts
··· 1 - import {Image as RNImage} from 'react-native-image-crop-picker' 2 - 3 - import {Dimensions} from './types' 1 + import {type PickerImage} from './picker.shared' 2 + import {type Dimensions} from './types' 4 3 import {blobToDataUri, getDataUriSize} from './util' 5 4 6 5 export async function compressIfNeeded( 7 - img: RNImage, 6 + img: PickerImage, 8 7 maxSize: number, 9 - ): Promise<RNImage> { 8 + ): Promise<PickerImage> { 10 9 if (img.size < maxSize) { 11 10 return img 12 11 } ··· 69 68 maxSize: number 70 69 } 71 70 72 - async function doResize(dataUri: string, opts: DoResizeOpts): Promise<RNImage> { 71 + async function doResize( 72 + dataUri: string, 73 + opts: DoResizeOpts, 74 + ): Promise<PickerImage> { 73 75 let newDataUri 74 76 75 77 let minQualityPercentage = 0
+7 -10
src/lib/media/picker.e2e.tsx
··· 1 - import { 2 - Image as RNImage, 3 - openCropper as openCropperFn, 4 - } from 'react-native-image-crop-picker' 5 1 import { 6 2 documentDirectory, 7 3 getInfoAsync, 8 4 readDirectoryAsync, 9 5 } from 'expo-file-system' 6 + import ExpoImageCropTool, {type OpenCropperOptions} from 'expo-image-crop-tool' 10 7 11 8 import {compressIfNeeded} from './manip' 12 - import {CropperOptions} from './types' 9 + import {type PickerImage} from './picker.shared' 13 10 14 11 async function getFile() { 15 12 const imagesDir = documentDirectory! ··· 37 34 }) 38 35 } 39 36 40 - export async function openPicker(): Promise<RNImage[]> { 37 + export async function openPicker(): Promise<PickerImage[]> { 41 38 return [await getFile()] 42 39 } 43 40 44 - export async function openCamera(): Promise<RNImage> { 41 + export async function openCamera(): Promise<PickerImage> { 45 42 return await getFile() 46 43 } 47 44 48 - export async function openCropper(opts: CropperOptions) { 49 - const item = await openCropperFn({ 45 + export async function openCropper(opts: OpenCropperOptions) { 46 + const item = await ExpoImageCropTool.openCropperAsync({ 50 47 ...opts, 51 - forceJpg: true, // ios only 48 + format: 'jpeg', 52 49 }) 53 50 54 51 return {
+9 -2
src/lib/media/picker.shared.ts
··· 1 1 import { 2 - ImagePickerOptions, 2 + type ImagePickerOptions, 3 3 launchImageLibraryAsync, 4 4 MediaTypeOptions, 5 5 } from 'expo-image-picker' 6 - // TODO: replace global i18n instance with one returned from useLingui -sfn 7 6 import {t} from '@lingui/macro' 8 7 9 8 import * as Toast from '#/view/com/util/Toast' 10 9 import {getDataUriSize} from './util' 10 + 11 + export type PickerImage = { 12 + mime: string 13 + height: number 14 + width: number 15 + path: string 16 + size: number 17 + } 11 18 12 19 export async function openPicker(opts?: ImagePickerOptions) { 13 20 const response = await launchImageLibraryAsync({
+23 -25
src/lib/media/picker.tsx
··· 1 - import { 2 - Image as RNImage, 3 - openCamera as openCameraFn, 4 - openCropper as openCropperFn, 5 - } from 'react-native-image-crop-picker' 1 + import ExpoImageCropTool, {type OpenCropperOptions} from 'expo-image-crop-tool' 2 + import {type ImagePickerOptions, launchCameraAsync} from 'expo-image-picker' 6 3 7 - import {CameraOpts, CropperOptions} from './types' 8 - export {openPicker} from './picker.shared' 4 + export {openPicker, type PickerImage as RNImage} from './picker.shared' 9 5 10 - export async function openCamera(opts: CameraOpts): Promise<RNImage> { 11 - const item = await openCameraFn({ 12 - width: opts.width, 13 - height: opts.height, 14 - freeStyleCropEnabled: opts.freeStyleCropEnabled, 15 - cropperCircleOverlay: opts.cropperCircleOverlay, 16 - cropping: false, 17 - forceJpg: true, // ios only 18 - compressImageQuality: 0.8, 19 - }) 6 + export async function openCamera(customOpts: ImagePickerOptions) { 7 + const opts: ImagePickerOptions = { 8 + mediaTypes: 'images', 9 + ...customOpts, 10 + } 11 + const res = await launchCameraAsync(opts) 12 + 13 + if (!res || !res.assets) { 14 + throw new Error('Camera was closed before taking a photo') 15 + } 16 + 17 + const asset = res?.assets[0] 20 18 21 19 return { 22 - path: item.path, 23 - mime: item.mime, 24 - size: item.size, 25 - width: item.width, 26 - height: item.height, 20 + path: asset.uri, 21 + mime: asset.mimeType ?? 'image/jpeg', 22 + size: asset.fileSize ?? 0, 23 + width: asset.width, 24 + height: asset.height, 27 25 } 28 26 } 29 27 30 - export async function openCropper(opts: CropperOptions) { 31 - const item = await openCropperFn({ 28 + export async function openCropper(opts: OpenCropperOptions) { 29 + const item = await ExpoImageCropTool.openCropperAsync({ 32 30 ...opts, 33 - forceJpg: true, // ios only 31 + format: 'jpeg', 34 32 }) 35 33 36 34 return {
+13 -13
src/lib/media/picker.web.tsx
··· 1 1 /// <reference lib="dom" /> 2 2 3 - import {Image as RNImage} from 'react-native-image-crop-picker' 3 + import {type OpenCropperOptions} from 'expo-image-crop-tool' 4 4 5 - import {CameraOpts, CropperOptions} from './types' 6 - export {openPicker} from './picker.shared' 7 5 import {unstable__openModal} from '#/state/modals' 6 + import {type PickerImage} from './picker.shared' 7 + import {type CameraOpts} from './types' 8 8 9 - export async function openCamera(_opts: CameraOpts): Promise<RNImage> { 9 + export {openPicker, type PickerImage as RNImage} from './picker.shared' 10 + 11 + export async function openCamera(_opts: CameraOpts): Promise<PickerImage> { 10 12 // const mediaType = opts.mediaType || 'photo' TODO 11 13 throw new Error('TODO') 12 14 } 13 15 14 - export async function openCropper(opts: CropperOptions): Promise<RNImage> { 16 + export async function openCropper( 17 + opts: OpenCropperOptions, 18 + ): Promise<PickerImage> { 15 19 // TODO handle more opts 16 20 return new Promise((resolve, reject) => { 17 21 unstable__openModal({ 18 22 name: 'crop-image', 19 - uri: opts.path, 20 - dimensions: 21 - opts.width && opts.height 22 - ? {width: opts.width, height: opts.height} 23 - : undefined, 24 - aspect: opts.webAspectRatio, 25 - circular: opts.webCircularCrop, 26 - onSelect: (img?: RNImage) => { 23 + uri: opts.imageUri, 24 + aspect: opts.aspectRatio, 25 + circular: opts.shape === 'circle', 26 + onSelect: (img?: PickerImage) => { 27 27 if (img) { 28 28 resolve(img) 29 29 } else {
-7
src/lib/media/types.ts
··· 1 - import {openCropper} from 'react-native-image-crop-picker' 2 - 3 1 export interface Dimensions { 4 2 width: number 5 3 height: number ··· 17 15 freeStyleCropEnabled?: boolean 18 16 cropperCircleOverlay?: boolean 19 17 } 20 - 21 - export type CropperOptions = Parameters<typeof openCropper>[0] & { 22 - webAspectRatio?: number 23 - webCircularCrop?: boolean 24 - }
+3 -5
src/screens/Onboarding/StepProfile/index.tsx
··· 182 182 183 183 if (!isWeb) { 184 184 image = await openCropper({ 185 - mediaType: 'photo', 186 - cropperCircleOverlay: true, 187 - height: 1000, 188 - width: 1000, 189 - path: image.path, 185 + imageUri: image.path, 186 + shape: 'circle', 187 + aspectRatio: 1 / 1, 190 188 }) 191 189 } 192 190 image = await compressIfNeeded(image, 1000000)
+5 -5
src/screens/Profile/Header/EditProfileDialog.tsx
··· 1 1 import {useCallback, useEffect, useState} from 'react' 2 2 import {Dimensions, View} from 'react-native' 3 - import {type Image as RNImage} from 'react-native-image-crop-picker' 4 3 import {type AppBskyActorDefs} from '@atproto/api' 5 4 import {msg, Plural, Trans} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' 7 6 8 7 import {urls} from '#/lib/constants' 9 8 import {compressIfNeeded} from '#/lib/media/manip' 9 + import {type PickerImage} from '#/lib/media/picker.shared' 10 10 import {cleanError} from '#/lib/strings/errors' 11 11 import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers' 12 12 import {logger} from '#/logger' ··· 127 127 profile.avatar, 128 128 ) 129 129 const [newUserBanner, setNewUserBanner] = useState< 130 - RNImage | undefined | null 130 + PickerImage | undefined | null 131 131 >() 132 132 const [newUserAvatar, setNewUserAvatar] = useState< 133 - RNImage | undefined | null 133 + PickerImage | undefined | null 134 134 >() 135 135 136 136 const dirty = ··· 144 144 }, [dirty, setDirty]) 145 145 146 146 const onSelectNewAvatar = useCallback( 147 - async (img: RNImage | null) => { 147 + async (img: PickerImage | null) => { 148 148 setImageError('') 149 149 if (img === null) { 150 150 setNewUserAvatar(null) ··· 163 163 ) 164 164 165 165 const onSelectNewBanner = useCallback( 166 - async (img: RNImage | null) => { 166 + async (img: PickerImage | null) => { 167 167 setImageError('') 168 168 if (!img) { 169 169 setNewUserBanner(null)
+2 -14
src/state/gallery.ts
··· 16 16 import {getImageDim} from '#/lib/media/manip' 17 17 import {openCropper} from '#/lib/media/picker' 18 18 import {getDataUriSize} from '#/lib/media/util' 19 - import {isIOS, isNative} from '#/platform/detection' 19 + import {isNative} from '#/platform/detection' 20 20 21 21 export type ImageTransformation = { 22 22 crop?: ActionCrop['crop'] ··· 122 122 return img 123 123 } 124 124 125 - // NOTE 126 - // on ios, react-native-image-crop-picker gives really bad quality 127 - // without specifying width and height. on android, however, the 128 - // crop stretches incorrectly if you do specify it. these are 129 - // both separate bugs in the library. we deal with that by 130 - // providing width & height for ios only 131 - // -prf 132 - 133 125 const source = img.source 134 - const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX) 135 126 136 127 // @todo: we're always passing the original image here, does image-cropper 137 128 // allows for setting initial crop dimensions? -mary 138 129 try { 139 130 const cropped = await openCropper({ 140 - mediaType: 'photo', 141 - path: source.path, 142 - freeStyleCropEnabled: true, 143 - ...(isIOS ? {width: w, height: h} : {}), 131 + imageUri: source.path, 144 132 }) 145 133 146 134 return {
+2 -2
src/state/modals/index.tsx
··· 1 1 import React from 'react' 2 - import {type Image as RNImage} from 'react-native-image-crop-picker' 3 2 import {type AppBskyActorDefs, type AppBskyGraphDefs} from '@atproto/api' 4 3 5 4 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 5 + import {type PickerImage} from '#/lib/media/picker.shared' 6 6 7 7 export interface EditProfileModal { 8 8 name: 'edit-profile' ··· 32 32 dimensions?: {width: number; height: number} 33 33 aspect?: number 34 34 circular?: boolean 35 - onSelect: (img?: RNImage) => void 35 + onSelect: (img?: PickerImage) => void 36 36 } 37 37 38 38 export interface DeleteAccountModal {
+11 -11
src/state/queries/list.ts
··· 1 - import {Image as RNImage} from 'react-native-image-crop-picker' 2 1 import { 3 - $Typed, 4 - AppBskyGraphDefs, 5 - AppBskyGraphGetList, 6 - AppBskyGraphList, 2 + type $Typed, 3 + type AppBskyGraphDefs, 4 + type AppBskyGraphGetList, 5 + type AppBskyGraphList, 7 6 AtUri, 8 - BskyAgent, 9 - ComAtprotoRepoApplyWrites, 10 - Facet, 11 - Un$Typed, 7 + type BskyAgent, 8 + type ComAtprotoRepoApplyWrites, 9 + type Facet, 10 + type Un$Typed, 12 11 } from '@atproto/api' 13 12 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 14 13 import chunk from 'lodash.chunk' 15 14 16 15 import {uploadBlob} from '#/lib/api' 17 16 import {until} from '#/lib/async/until' 17 + import {type PickerImage} from '#/lib/media/picker.shared' 18 18 import {STALE} from '#/state/queries' 19 19 import {useAgent, useSession} from '../session' 20 20 import {invalidate as invalidateMyLists} from './my-lists' ··· 47 47 name: string 48 48 description: string 49 49 descriptionFacets: Facet[] | undefined 50 - avatar: RNImage | null | undefined 50 + avatar: PickerImage | null | undefined 51 51 } 52 52 export function useListCreateMutation() { 53 53 const {currentAccount} = useSession() ··· 115 115 name: string 116 116 description: string 117 117 descriptionFacets: Facet[] | undefined 118 - avatar: RNImage | null | undefined 118 + avatar: PickerImage | null | undefined 119 119 } 120 120 export function useListMetadataMutation() { 121 121 const {currentAccount} = useSession()
+3 -3
src/state/queries/profile.ts
··· 1 1 import {useCallback} from 'react' 2 - import {type Image as RNImage} from 'react-native-image-crop-picker' 3 2 import { 4 3 type AppBskyActorDefs, 5 4 type AppBskyActorGetProfile, ··· 21 20 import {uploadBlob} from '#/lib/api' 22 21 import {until} from '#/lib/async/until' 23 22 import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' 23 + import {type PickerImage} from '#/lib/media/picker.shared' 24 24 import {logEvent, type LogEvents, toClout} from '#/lib/statsig/statsig' 25 25 import {type Shadow} from '#/state/cache/types' 26 26 import {STALE} from '#/state/queries' ··· 131 131 | (( 132 132 existing: Un$Typed<AppBskyActorProfile.Record>, 133 133 ) => Un$Typed<AppBskyActorProfile.Record>) 134 - newUserAvatar?: RNImage | undefined | null 135 - newUserBanner?: RNImage | undefined | null 134 + newUserAvatar?: PickerImage | undefined | null 135 + newUserBanner?: PickerImage | undefined | null 136 136 checkCommitted?: (res: AppBskyActorGetProfile.Response) => boolean 137 137 } 138 138 export function useProfileUpdateMutation() {
+1 -3
src/view/com/composer/photos/OpenCameraBtn.tsx
··· 35 35 } 36 36 37 37 const img = await openCamera({ 38 - width: POST_IMG_MAX.width, 39 - height: POST_IMG_MAX.height, 40 - freeStyleCropEnabled: true, 38 + aspect: [POST_IMG_MAX.width, POST_IMG_MAX.height], 41 39 }) 42 40 43 41 // If we don't have permissions it's fine, we just wont save it. The post itself will still have access to
+3 -3
src/view/com/modals/CreateOrEditList.tsx
··· 8 8 TouchableOpacity, 9 9 View, 10 10 } from 'react-native' 11 - import {type Image as RNImage} from 'react-native-image-crop-picker' 12 11 import {LinearGradient} from 'expo-linear-gradient' 13 12 import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' 14 13 import {msg, Trans} from '@lingui/macro' ··· 17 16 import {usePalette} from '#/lib/hooks/usePalette' 18 17 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 19 18 import {compressIfNeeded} from '#/lib/media/manip' 19 + import {type PickerImage} from '#/lib/media/picker.shared' 20 20 import {cleanError, isNetworkError} from '#/lib/strings/errors' 21 21 import {enforceLen} from '#/lib/strings/helpers' 22 22 import {richTextToString} from '#/lib/strings/rich-text-helpers' ··· 95 95 const isDescriptionOver = graphemeLength > MAX_DESCRIPTION 96 96 97 97 const [avatar, setAvatar] = useState<string | undefined>(list?.avatar) 98 - const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() 98 + const [newAvatar, setNewAvatar] = useState<PickerImage | undefined | null>() 99 99 100 100 const onDescriptionChange = useCallback( 101 101 (newText: string) => { ··· 112 112 }, [closeModal]) 113 113 114 114 const onSelectNewAvatar = useCallback( 115 - async (img: RNImage | null) => { 115 + async (img: PickerImage | null) => { 116 116 if (!img) { 117 117 setNewAvatar(null) 118 118 setAvatar(undefined)
+3 -3
src/view/com/modals/CropImage.web.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {Image as RNImage} from 'react-native-image-crop-picker' 4 3 import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' 5 4 import {LinearGradient} from 'expo-linear-gradient' 6 5 import {msg, Trans} from '@lingui/macro' 7 6 import {useLingui} from '@lingui/react' 8 - import ReactCrop, {PercentCrop} from 'react-image-crop' 7 + import ReactCrop, {type PercentCrop} from 'react-image-crop' 9 8 10 9 import {usePalette} from '#/lib/hooks/usePalette' 11 10 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 + import {type PickerImage} from '#/lib/media/picker.shared' 12 12 import {getDataUriSize} from '#/lib/media/util' 13 13 import {gradients, s} from '#/lib/styles' 14 14 import {useModalControls} from '#/state/modals' ··· 25 25 uri: string 26 26 aspect?: number 27 27 circular?: boolean 28 - onSelect: (img?: RNImage) => void 28 + onSelect: (img?: PickerImage) => void 29 29 }) { 30 30 const pal = usePalette('default') 31 31 const {_} = useLingui()
+5 -5
src/view/com/modals/EditProfile.tsx
··· 8 8 TouchableOpacity, 9 9 View, 10 10 } from 'react-native' 11 - import {type Image as RNImage} from 'react-native-image-crop-picker' 12 11 import Animated, {FadeOut} from 'react-native-reanimated' 13 12 import {LinearGradient} from 'expo-linear-gradient' 14 13 import {type AppBskyActorDefs} from '@atproto/api' ··· 18 17 import {MAX_DESCRIPTION, MAX_DISPLAY_NAME, urls} from '#/lib/constants' 19 18 import {usePalette} from '#/lib/hooks/usePalette' 20 19 import {compressIfNeeded} from '#/lib/media/manip' 20 + import {type PickerImage} from '#/lib/media/picker.shared' 21 21 import {cleanError} from '#/lib/strings/errors' 22 22 import {enforceLen} from '#/lib/strings/helpers' 23 23 import {colors, gradients, s} from '#/lib/styles' ··· 67 67 profile.avatar, 68 68 ) 69 69 const [newUserBanner, setNewUserBanner] = useState< 70 - RNImage | undefined | null 70 + PickerImage | undefined | null 71 71 >() 72 72 const [newUserAvatar, setNewUserAvatar] = useState< 73 - RNImage | undefined | null 73 + PickerImage | undefined | null 74 74 >() 75 75 const onPressCancel = () => { 76 76 closeModal() 77 77 } 78 78 const onSelectNewAvatar = useCallback( 79 - async (img: RNImage | null) => { 79 + async (img: PickerImage | null) => { 80 80 setImageError('') 81 81 if (img === null) { 82 82 setNewUserAvatar(null) ··· 95 95 ) 96 96 97 97 const onSelectNewBanner = useCallback( 98 - async (img: RNImage | null) => { 98 + async (img: PickerImage | null) => { 99 99 setImageError('') 100 100 if (!img) { 101 101 setNewUserBanner(null)
+14 -17
src/view/com/util/UserAvatar.tsx
··· 2 2 import { 3 3 Image, 4 4 Pressable, 5 - StyleProp, 5 + type StyleProp, 6 6 StyleSheet, 7 7 View, 8 - ViewStyle, 8 + type ViewStyle, 9 9 } from 'react-native' 10 - import {Image as RNImage} from 'react-native-image-crop-picker' 11 10 import Svg, {Circle, Path, Rect} from 'react-native-svg' 12 - import {ModerationUI} from '@atproto/api' 11 + import {type ModerationUI} from '@atproto/api' 13 12 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 14 13 import {msg, Trans} from '@lingui/macro' 15 14 import {useLingui} from '@lingui/react' ··· 38 37 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 39 38 import * as Menu from '#/components/Menu' 40 39 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 41 - import * as bsky from '#/types/bsky' 42 - import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' 40 + import type * as bsky from '#/types/bsky' 41 + import { 42 + openCamera, 43 + openCropper, 44 + openPicker, 45 + type RNImage, 46 + } from '../../../lib/media/picker' 43 47 44 48 export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' 45 49 ··· 312 316 313 317 onSelectNewAvatar( 314 318 await openCamera({ 315 - width: 1000, 316 - height: 1000, 317 - cropperCircleOverlay: true, 319 + aspect: [1, 1], 318 320 }), 319 321 ) 320 322 }, [onSelectNewAvatar, requestCameraAccessIfNeeded]) ··· 336 338 337 339 try { 338 340 const croppedImage = await openCropper({ 339 - mediaType: 'photo', 340 - cropperCircleOverlay: true, 341 - height: 1000, 342 - width: 1000, 343 - path: item.path, 344 - webAspectRatio: 1, 345 - webCircularCrop: true, 341 + imageUri: item.path, 342 + shape: 'circle', 343 + aspectRatio: 1, 346 344 }) 347 - 348 345 onSelectNewAvatar(croppedImage) 349 346 } catch (e: any) { 350 347 // Don't log errors for cancelling selection to sentry on ios or android
+10 -10
src/view/com/util/UserBanner.tsx
··· 1 1 import React from 'react' 2 2 import {Pressable, StyleSheet, View} from 'react-native' 3 - import {Image as RNImage} from 'react-native-image-crop-picker' 4 3 import {Image} from 'expo-image' 5 - import {ModerationUI} from '@atproto/api' 4 + import {type ModerationUI} from '@atproto/api' 6 5 import {msg, Trans} from '@lingui/macro' 7 6 import {useLingui} from '@lingui/react' 8 7 ··· 25 24 import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' 26 25 import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' 27 26 import * as Menu from '#/components/Menu' 28 - import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' 27 + import { 28 + openCamera, 29 + openCropper, 30 + openPicker, 31 + type RNImage, 32 + } from '../../../lib/media/picker' 29 33 30 34 export function UserBanner({ 31 35 type, ··· 52 56 } 53 57 onSelectNewBanner?.( 54 58 await openCamera({ 55 - width: 3000, 56 - height: 1000, 59 + aspect: [3, 1], 57 60 }), 58 61 ) 59 62 }, [onSelectNewBanner, requestCameraAccessIfNeeded]) ··· 70 73 try { 71 74 onSelectNewBanner?.( 72 75 await openCropper({ 73 - mediaType: 'photo', 74 - path: items[0].path, 75 - width: 3000, 76 - height: 1000, 77 - webAspectRatio: 3, 76 + imageUri: items[0].path, 77 + aspectRatio: 3 / 1, 78 78 }), 79 79 ) 80 80 } catch (e: any) {
+5 -5
yarn.lock
··· 11247 11247 resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-14.1.4.tgz#442f48b1bdf83484d4fcadc653445aaae6049b70" 11248 11248 integrity sha512-QZdE3NMX74rTuIl82I+n12XGwpDWKb8zfs5EpwsnGi/D/n7O2Jd4tO5ivH+muEG/OCJOMq5aeaVDqqaQOhTkcA== 11249 11249 11250 + expo-image-crop-tool@^0.1.7: 11251 + version "0.1.7" 11252 + resolved "https://registry.yarnpkg.com/expo-image-crop-tool/-/expo-image-crop-tool-0.1.7.tgz#a84ed2192d147d922b3d352e52e29bc3a4c1e800" 11253 + integrity sha512-An+tszv0DKHA74Yr7uQb4mqGTxTVBwku9zu8yvhb7HzBXIUGw12hnb8M6ntHZqIFuQiLzBxaKH8DTwZgg9oAnw== 11254 + 11250 11255 expo-image-loader@~5.1.0: 11251 11256 version "5.1.0" 11252 11257 resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-5.1.0.tgz#f7d65f9b9a9714eaaf5d50a406cb34cb25262153" ··· 16795 16800 integrity sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ== 16796 16801 dependencies: 16797 16802 fast-base64-decode "^1.0.0" 16798 - 16799 - react-native-image-crop-picker@^0.42.0: 16800 - version "0.42.0" 16801 - resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.42.0.tgz#0672c080feb8ffefd65ba00a4e64bc8a1066136e" 16802 - integrity sha512-EOEkekPJ7g+CNf92HrWAGM4kcDJyVY02gQJUVH7MSNUOK11SHnurXVM0TnwIt410Y4T+lBkq3rfJEA1qDaDDwA== 16803 16803 16804 16804 react-native-ios-context-menu@^1.15.3: 16805 16805 version "1.15.3"