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>
This commit is contained in:
parent
973538d246
commit
521ec8e044
9
__mocks__/react-native-image-crop-picker.js
vendored
9
__mocks__/react-native-image-crop-picker.js
vendored
@ -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: ''}))
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
10
yarn.lock
10
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user