Revamp image editor (#5462)
* new image editor * Rm react-avatar-editor --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
import {ComposerImage} from '#/state/gallery'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
|
||||
export type EditImageDialogProps = {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
image: ComposerImage
|
||||
onChange: (next: ComposerImage) => void
|
||||
}
|
||||
|
||||
export const EditImageDialog = ({}: EditImageDialogProps): React.ReactNode => {
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import ReactCrop, {PercentCrop} from 'react-image-crop'
|
||||
|
||||
import {
|
||||
ImageSource,
|
||||
ImageTransformation,
|
||||
manipulateImage,
|
||||
} from '#/state/gallery'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {EditImageDialogProps} from './EditImageDialog'
|
||||
|
||||
export const EditImageDialog = (props: EditImageDialogProps) => {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<EditImageInner key={props.image.source.id} {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => {
|
||||
const {_} = useLingui()
|
||||
|
||||
const source = image.source
|
||||
|
||||
const initialCrop = getInitialCrop(source, image.manips)
|
||||
const [crop, setCrop] = React.useState(initialCrop)
|
||||
|
||||
const isEmpty = !crop || (crop.width || crop.height) === 0
|
||||
const isNew = initialCrop ? true : !isEmpty
|
||||
|
||||
const onPressSubmit = React.useCallback(async () => {
|
||||
const result = await manipulateImage(image, {
|
||||
crop:
|
||||
crop && (crop.width || crop.height) !== 0
|
||||
? {
|
||||
originX: (crop.x * source.width) / 100,
|
||||
originY: (crop.y * source.height) / 100,
|
||||
width: (crop.width * source.width) / 100,
|
||||
height: (crop.height * source.height) / 100,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
onChange(result)
|
||||
control.close()
|
||||
}, [crop, image, source, control, onChange])
|
||||
|
||||
return (
|
||||
<Dialog.Inner label={_(msg`Edit image`)}>
|
||||
<Dialog.Close />
|
||||
|
||||
<Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_sm]}>
|
||||
<Trans>Edit image</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.align_center]}>
|
||||
<ReactCrop
|
||||
crop={crop}
|
||||
onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)}
|
||||
className="ReactCrop--no-animate">
|
||||
<img src={source.path} style={{maxHeight: `50vh`}} />
|
||||
</ReactCrop>
|
||||
</View>
|
||||
|
||||
<View style={[a.mt_md, a.gap_md]}>
|
||||
<Button
|
||||
disabled={!isNew}
|
||||
label={_(msg`Save`)}
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={onPressSubmit}>
|
||||
<ButtonText>
|
||||
<Trans>Save</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</Dialog.Inner>
|
||||
)
|
||||
}
|
||||
|
||||
const getInitialCrop = (
|
||||
source: ImageSource,
|
||||
manips: ImageTransformation | undefined,
|
||||
): PercentCrop | undefined => {
|
||||
const initialArea = manips?.crop
|
||||
|
||||
if (initialArea) {
|
||||
return {
|
||||
unit: '%',
|
||||
x: (initialArea.originX / source.width) * 100,
|
||||
y: (initialArea.originY / source.height) * 100,
|
||||
width: (initialArea.width / source.width) * 100,
|
||||
height: (initialArea.height / source.height) * 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {EditImageDialog} from './EditImageDialog'
|
||||
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
||||
|
||||
const IMAGE_GAP = 8
|
||||
@@ -144,12 +145,15 @@ const GalleryItem = ({
|
||||
const t = useTheme()
|
||||
|
||||
const altTextControl = Dialog.useDialogControl()
|
||||
const editControl = Dialog.useDialogControl()
|
||||
|
||||
const onImageEdit = () => {
|
||||
if (isNative) {
|
||||
cropImage(image).then(next => {
|
||||
onChange(next)
|
||||
})
|
||||
} else {
|
||||
editControl.open()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,21 +189,15 @@ const GalleryItem = ({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={imageControlsStyle}>
|
||||
{isNative && (
|
||||
<TouchableOpacity
|
||||
testID="editPhotoButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Edit image`)}
|
||||
accessibilityHint=""
|
||||
onPress={onImageEdit}
|
||||
style={styles.imageControl}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={{color: colors.white}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
testID="editPhotoButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Edit image`)}
|
||||
accessibilityHint=""
|
||||
onPress={onImageEdit}
|
||||
style={styles.imageControl}>
|
||||
<FontAwesomeIcon icon="pen" size={12} style={{color: colors.white}} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="removePhotoButton"
|
||||
accessibilityRole="button"
|
||||
@@ -237,6 +235,12 @@ const GalleryItem = ({
|
||||
image={image}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<EditImageDialog
|
||||
control={editControl}
|
||||
image={image}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
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 {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {getDataUriSize} from '#/lib/media/util'
|
||||
import {gradients, s} from '#/lib/styles'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
|
||||
export const snapPoints = ['0%']
|
||||
|
||||
export function Component({
|
||||
uri,
|
||||
aspect,
|
||||
circular,
|
||||
onSelect,
|
||||
}: {
|
||||
uri: string
|
||||
aspect?: number
|
||||
circular?: boolean
|
||||
onSelect: (img?: RNImage) => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
const imageRef = React.useRef<HTMLImageElement>(null)
|
||||
const [crop, setCrop] = React.useState<PercentCrop>()
|
||||
|
||||
const isEmpty = !crop || (crop.width || crop.height) === 0
|
||||
|
||||
const onPressCancel = () => {
|
||||
onSelect(undefined)
|
||||
closeModal()
|
||||
}
|
||||
const onPressDone = async () => {
|
||||
const img = imageRef.current!
|
||||
|
||||
const result = await manipulateAsync(
|
||||
uri,
|
||||
isEmpty
|
||||
? []
|
||||
: [
|
||||
{
|
||||
crop: {
|
||||
originX: (crop.x * img.naturalWidth) / 100,
|
||||
originY: (crop.y * img.naturalHeight) / 100,
|
||||
width: (crop.width * img.naturalWidth) / 100,
|
||||
height: (crop.height * img.naturalHeight) / 100,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
base64: true,
|
||||
format: SaveFormat.JPEG,
|
||||
},
|
||||
)
|
||||
|
||||
onSelect({
|
||||
path: result.uri,
|
||||
mime: 'image/jpeg',
|
||||
size: result.base64 !== undefined ? getDataUriSize(result.base64) : 0,
|
||||
width: result.width,
|
||||
height: result.height,
|
||||
})
|
||||
|
||||
closeModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[styles.cropper, pal.borderDark]}>
|
||||
<ReactCrop
|
||||
aspect={aspect}
|
||||
crop={crop}
|
||||
onChange={(_pixelCrop, percentCrop) => setCrop(percentCrop)}
|
||||
circularCrop={circular}>
|
||||
<img ref={imageRef} src={uri} style={{maxHeight: '75vh'}} />
|
||||
</ReactCrop>
|
||||
</View>
|
||||
<View style={[styles.btns, isMobile && {paddingHorizontal: 16}]}>
|
||||
<TouchableOpacity
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Cancel image crop`)}
|
||||
accessibilityHint={_(msg`Exits image cropping process`)}>
|
||||
<Text type="xl" style={pal.link}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity
|
||||
onPress={onPressDone}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Save image crop`)}
|
||||
accessibilityHint={_(msg`Saves image crop settings`)}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text type="xl-medium" style={s.white}>
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cropper: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
},
|
||||
ctrls: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
btns: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
btn: {
|
||||
borderRadius: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
})
|
||||
@@ -12,7 +12,7 @@ import * as ChangeEmailModal from './ChangeEmail'
|
||||
import * as ChangeHandleModal from './ChangeHandle'
|
||||
import * as ChangePasswordModal from './ChangePassword'
|
||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||
import * as CropImageModal from './crop-image/CropImage.web'
|
||||
import * as CropImageModal from './CropImage.web'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as EditProfileModal from './EditProfile'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Slider} from '@miblanchard/react-native-slider'
|
||||
import ImageEditor from 'react-avatar-editor'
|
||||
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons'
|
||||
import {Dimensions} from 'lib/media/types'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
import {gradients, s} from 'lib/styles'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {calculateDimensions} from './cropImageUtil'
|
||||
|
||||
enum AspectRatio {
|
||||
Square = 'square',
|
||||
Wide = 'wide',
|
||||
Tall = 'tall',
|
||||
Custom = 'custom',
|
||||
}
|
||||
|
||||
const DIMS: Record<string, Dimensions> = {
|
||||
[AspectRatio.Square]: {width: 1000, height: 1000},
|
||||
[AspectRatio.Wide]: {width: 1000, height: 750},
|
||||
[AspectRatio.Tall]: {width: 750, height: 1000},
|
||||
}
|
||||
|
||||
export const snapPoints = ['0%']
|
||||
|
||||
export function Component({
|
||||
uri,
|
||||
dimensions,
|
||||
onSelect,
|
||||
}: {
|
||||
uri: string
|
||||
dimensions?: Dimensions
|
||||
onSelect: (img?: RNImage) => void
|
||||
}) {
|
||||
const {closeModal} = useModalControls()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const defaultAspectStyle = dimensions
|
||||
? AspectRatio.Custom
|
||||
: AspectRatio.Square
|
||||
const [as, setAs] = React.useState<AspectRatio>(defaultAspectStyle)
|
||||
const [scale, setScale] = React.useState<number>(1)
|
||||
const editorRef = React.useRef<ImageEditor>(null)
|
||||
const imageEditorWidth = dimensions ? dimensions.width : DIMS[as].width
|
||||
const imageEditorHeight = dimensions ? dimensions.height : DIMS[as].height
|
||||
|
||||
const doSetAs = (v: AspectRatio) => () => setAs(v)
|
||||
|
||||
const onPressCancel = () => {
|
||||
onSelect(undefined)
|
||||
closeModal()
|
||||
}
|
||||
const onPressDone = () => {
|
||||
const canvas = editorRef.current?.getImageScaledToCanvas()
|
||||
if (canvas) {
|
||||
const dataUri = canvas.toDataURL('image/jpeg')
|
||||
onSelect({
|
||||
path: dataUri,
|
||||
mime: 'image/jpeg',
|
||||
size: getDataUriSize(dataUri),
|
||||
width: imageEditorWidth,
|
||||
height: imageEditorHeight,
|
||||
})
|
||||
} else {
|
||||
onSelect(undefined)
|
||||
}
|
||||
closeModal()
|
||||
}
|
||||
|
||||
let cropperStyle
|
||||
if (as === AspectRatio.Square) {
|
||||
cropperStyle = styles.cropperSquare
|
||||
} else if (as === AspectRatio.Wide) {
|
||||
cropperStyle = styles.cropperWide
|
||||
} else if (as === AspectRatio.Tall) {
|
||||
cropperStyle = styles.cropperTall
|
||||
} else if (as === AspectRatio.Custom) {
|
||||
const cropperDimensions = calculateDimensions(
|
||||
550,
|
||||
imageEditorHeight,
|
||||
imageEditorWidth,
|
||||
)
|
||||
cropperStyle = {
|
||||
width: cropperDimensions.width,
|
||||
height: cropperDimensions.height,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[styles.cropper, pal.borderDark, cropperStyle]}>
|
||||
<ImageEditor
|
||||
ref={editorRef}
|
||||
style={styles.imageEditor}
|
||||
image={uri}
|
||||
width={imageEditorWidth}
|
||||
height={imageEditorHeight}
|
||||
scale={scale}
|
||||
border={0}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.ctrls}>
|
||||
<Slider
|
||||
value={scale}
|
||||
onValueChange={(v: number | number[]) =>
|
||||
setScale(Array.isArray(v) ? v[0] : v)
|
||||
}
|
||||
minimumValue={1}
|
||||
maximumValue={3}
|
||||
containerStyle={styles.slider}
|
||||
/>
|
||||
{as === AspectRatio.Custom ? null : (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Wide)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Wide`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to wide`)}>
|
||||
<RectWideIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Wide ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Tall)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Tall`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to tall`)}>
|
||||
<RectTallIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Tall ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Square)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Square`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to square`)}>
|
||||
<SquareIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Square ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.btns}>
|
||||
<TouchableOpacity
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Cancel image crop`)}
|
||||
accessibilityHint={_(msg`Exits image cropping process`)}>
|
||||
<Text type="xl" style={pal.link}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity
|
||||
onPress={onPressDone}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Save image crop`)}
|
||||
accessibilityHint={_(msg`Saves image crop settings`)}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text type="xl-medium" style={s.white}>
|
||||
<Trans>Done</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cropper: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
cropperSquare: {
|
||||
width: 400,
|
||||
height: 400,
|
||||
},
|
||||
cropperWide: {
|
||||
width: 400,
|
||||
height: 300,
|
||||
},
|
||||
cropperTall: {
|
||||
width: 300,
|
||||
height: 400,
|
||||
},
|
||||
imageEditor: {
|
||||
maxWidth: '100%',
|
||||
},
|
||||
ctrls: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
slider: {
|
||||
flex: 1,
|
||||
marginRight: 10,
|
||||
},
|
||||
btns: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
btn: {
|
||||
borderRadius: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
export const calculateDimensions = (
|
||||
maxWidth: number,
|
||||
originalHeight: number,
|
||||
originalWidth: number,
|
||||
) => {
|
||||
const aspectRatio = originalWidth / originalHeight
|
||||
const newHeight = maxWidth / aspectRatio
|
||||
const newWidth = maxWidth
|
||||
return {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,17 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {
|
||||
useCameraPermission,
|
||||
usePhotoLibraryPermission,
|
||||
} from 'lib/hooks/usePermissions'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {colors} from 'lib/styles'
|
||||
import {isAndroid, isNative, isWeb} from 'platform/detection'
|
||||
import {precacheProfile} from 'state/queries/profile'
|
||||
import {HighPriorityImage} from 'view/com/util/images/Image'
|
||||
} from '#/lib/hooks/usePermissions'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {colors} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {isAndroid, isNative, isWeb} from '#/platform/detection'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {HighPriorityImage} from '#/view/com/util/images/Image'
|
||||
import {tokens, useTheme} from '#/alf'
|
||||
import {
|
||||
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||
@@ -321,6 +321,8 @@ let EditableUserAvatar = ({
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
path: item.path,
|
||||
webAspectRatio: 1,
|
||||
webCircularCrop: true,
|
||||
})
|
||||
|
||||
onSelectNewAvatar(croppedImage)
|
||||
|
||||
@@ -6,16 +6,16 @@ import {ModerationUI} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {
|
||||
useCameraPermission,
|
||||
usePhotoLibraryPermission,
|
||||
} from 'lib/hooks/usePermissions'
|
||||
import {colors} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {isAndroid, isNative} from 'platform/detection'
|
||||
import {EventStopper} from 'view/com/util/EventStopper'
|
||||
} from '#/lib/hooks/usePermissions'
|
||||
import {colors} from '#/lib/styles'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {logger} from '#/logger'
|
||||
import {isAndroid, isNative} from '#/platform/detection'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {tokens, useTheme as useAlfTheme} from '#/alf'
|
||||
import {
|
||||
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||
@@ -72,6 +72,7 @@ export function UserBanner({
|
||||
path: items[0].path,
|
||||
width: 3000,
|
||||
height: 1000,
|
||||
webAspectRatio: 3,
|
||||
}),
|
||||
)
|
||||
} catch (e: any) {
|
||||
|
||||
Reference in New Issue
Block a user