Break out the web/native image picking code and make some progress on the web version
parent
0673129b20
commit
7916b26aad
|
@ -25,6 +25,7 @@
|
|||
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
||||
"@gorhom/bottom-sheet": "^4",
|
||||
"@mattermost/react-native-paste-input": "^0.6.0",
|
||||
"@miblanchard/react-native-slider": "^2.2.0",
|
||||
"@notifee/react-native": "^7.4.0",
|
||||
"@react-native-async-storage/async-storage": "^1.17.6",
|
||||
"@react-native-camera-roll/camera-roll": "^5.2.2",
|
||||
|
@ -42,6 +43,7 @@
|
|||
"mobx": "^6.6.1",
|
||||
"mobx-react-lite": "^3.4.0",
|
||||
"react": "18.2.0",
|
||||
"react-avatar-editor": "^13.0.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-native": "0.71.0",
|
||||
|
@ -84,6 +86,7 @@
|
|||
"@types/jest": "^26.0.23",
|
||||
"@types/lodash.chunk": "^4.2.7",
|
||||
"@types/lodash.omit": "^4.5.7",
|
||||
"@types/react-avatar-editor": "^13.0.0",
|
||||
"@types/react-native": "^0.67.3",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
#app-root { display:flex; height:100%; }
|
||||
|
||||
/* Remove focus state on inputs */
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
*:focus {
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {Image as PickedImage} from '../../view/com/util/images/ImageCropPicker'
|
||||
import {Image as PickedImage} from '../../view/com/util/images/image-crop-picker/ImageCropPicker'
|
||||
import {
|
||||
AppBskyActorGetProfile as GetProfile,
|
||||
AppBskyActorProfile as Profile,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {ProfileViewModel} from './profile-view'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {PickedMedia} from '../../view/com/util/images/image-crop-picker/types'
|
||||
|
||||
export class ConfirmModal {
|
||||
name = 'confirm'
|
||||
|
@ -52,6 +53,17 @@ export class ReportAccountModal {
|
|||
}
|
||||
}
|
||||
|
||||
export class CropImageModal {
|
||||
name = 'crop-image'
|
||||
|
||||
constructor(
|
||||
public uri: string,
|
||||
public onSelect: (img?: PickedMedia) => void,
|
||||
) {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
interface LightboxModel {}
|
||||
|
||||
export class ProfileImageLightbox implements LightboxModel {
|
||||
|
@ -98,6 +110,7 @@ export class ShellUiModel {
|
|||
| ServerInputModal
|
||||
| ReportPostModal
|
||||
| ReportAccountModal
|
||||
| CropImageModal
|
||||
| undefined
|
||||
isLightboxActive = false
|
||||
activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined
|
||||
|
@ -140,7 +153,8 @@ export class ShellUiModel {
|
|||
| EditProfileModal
|
||||
| ServerInputModal
|
||||
| ReportPostModal
|
||||
| ReportAccountModal,
|
||||
| ReportAccountModal
|
||||
| CropImageModal,
|
||||
) {
|
||||
this.isModalActive = true
|
||||
this.activeModal = modal
|
||||
|
|
|
@ -16,14 +16,9 @@ export class UserLocalPhotosModel {
|
|||
}
|
||||
|
||||
async setup() {
|
||||
await this._getPhotos()
|
||||
}
|
||||
|
||||
private async _getPhotos() {
|
||||
CameraRoll.getPhotos({first: 20}).then(r => {
|
||||
runInAction(() => {
|
||||
this.photos = r.edges
|
||||
})
|
||||
const r = await CameraRoll.getPhotos({first: 20})
|
||||
runInAction(() => {
|
||||
this.photos = r.edges
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,8 +37,7 @@ import {
|
|||
} from '../../../lib/strings'
|
||||
import {getLinkMeta} from '../../../lib/link-meta'
|
||||
import {downloadAndResize} from '../../../lib/images'
|
||||
import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
|
||||
import {PhotoCarouselPicker, cropPhoto} from './PhotoCarouselPicker'
|
||||
import {PhotoCarouselPicker, cropPhoto} from './photos/PhotoCarouselPicker'
|
||||
import {SelectedPhoto} from './SelectedPhoto'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
|
||||
|
@ -77,10 +76,6 @@ export const ComposePost = observer(function ComposePost({
|
|||
() => new UserAutocompleteViewModel(store),
|
||||
[store],
|
||||
)
|
||||
const localPhotos = React.useMemo<UserLocalPhotosModel>(
|
||||
() => new UserLocalPhotosModel(store),
|
||||
[store],
|
||||
)
|
||||
|
||||
// HACK
|
||||
// there's a bug with @mattermost/react-native-paste-input where if the input
|
||||
|
@ -95,8 +90,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
// initial setup
|
||||
useEffect(() => {
|
||||
autocompleteView.setup()
|
||||
localPhotos.setup()
|
||||
}, [autocompleteView, localPhotos])
|
||||
}, [autocompleteView])
|
||||
|
||||
// external link metadata-fetch flow
|
||||
useEffect(() => {
|
||||
|
@ -220,7 +214,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
}
|
||||
const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri))
|
||||
if (imgUri) {
|
||||
const finalImgPath = await cropPhoto(imgUri)
|
||||
const finalImgPath = await cropPhoto(store, imgUri)
|
||||
onSelectPhotos([...selectedPhotos, finalImgPath])
|
||||
}
|
||||
}
|
||||
|
@ -412,15 +406,12 @@ export const ComposePost = observer(function ComposePost({
|
|||
/>
|
||||
)}
|
||||
</ScrollView>
|
||||
{isSelectingPhotos &&
|
||||
localPhotos.photos != null &&
|
||||
selectedPhotos.length < 4 && (
|
||||
<PhotoCarouselPicker
|
||||
selectedPhotos={selectedPhotos}
|
||||
onSelectPhotos={onSelectPhotos}
|
||||
localPhotos={localPhotos}
|
||||
/>
|
||||
)}
|
||||
{isSelectingPhotos && selectedPhotos.length < 4 && (
|
||||
<PhotoCarouselPicker
|
||||
selectedPhotos={selectedPhotos}
|
||||
onSelectPhotos={onSelectPhotos}
|
||||
/>
|
||||
)}
|
||||
<View style={[pal.border, styles.bottomBar]}>
|
||||
<TouchableOpacity
|
||||
testID="composerSelectPhotosButton"
|
||||
|
|
|
@ -8,14 +8,14 @@ import {
|
|||
openPicker,
|
||||
openCamera,
|
||||
openCropper,
|
||||
} from '../util/images/ImageCropPicker'
|
||||
} from '../../util/images/image-crop-picker/ImageCropPicker'
|
||||
import {
|
||||
UserLocalPhotosModel,
|
||||
PhotoIdentifier,
|
||||
} from '../../../state/models/user-local-photos'
|
||||
import {compressIfNeeded, scaleDownDimensions} from '../../../lib/images'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
import {useStores} from '../../../state'
|
||||
} from '../../../../state/models/user-local-photos'
|
||||
import {compressIfNeeded, scaleDownDimensions} from '../../../../lib/images'
|
||||
import {usePalette} from '../../../lib/hooks/usePalette'
|
||||
import {useStores, RootStoreModel} from '../../../../state'
|
||||
|
||||
const MAX_WIDTH = 1000
|
||||
const MAX_HEIGHT = 1000
|
||||
|
@ -25,11 +25,10 @@ const IMAGE_PARAMS = {
|
|||
width: 1000,
|
||||
height: 1000,
|
||||
freeStyleCropEnabled: true,
|
||||
forceJpg: true, // ios only
|
||||
compressImageQuality: 1.0,
|
||||
}
|
||||
|
||||
export async function cropPhoto(
|
||||
store: RootStoreModel,
|
||||
path: string,
|
||||
imgWidth = MAX_WIDTH,
|
||||
imgHeight = MAX_HEIGHT,
|
||||
|
@ -40,10 +39,10 @@ export async function cropPhoto(
|
|||
{width: imgWidth, height: imgHeight},
|
||||
{width: MAX_WIDTH, height: MAX_HEIGHT},
|
||||
)
|
||||
const cropperRes = await openCropper({
|
||||
const cropperRes = await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
path,
|
||||
...IMAGE_PARAMS,
|
||||
freeStyleCropEnabled: true,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
|
@ -54,19 +53,30 @@ export async function cropPhoto(
|
|||
export const PhotoCarouselPicker = ({
|
||||
selectedPhotos,
|
||||
onSelectPhotos,
|
||||
localPhotos,
|
||||
}: {
|
||||
selectedPhotos: string[]
|
||||
onSelectPhotos: (v: string[]) => void
|
||||
localPhotos: UserLocalPhotosModel
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const [localPhotos, setLocalPhotos] = React.useState<
|
||||
UserLocalPhotosModel | undefined
|
||||
>(undefined)
|
||||
|
||||
// initial setup
|
||||
React.useEffect(() => {
|
||||
const photos = new UserLocalPhotosModel(store)
|
||||
photos.setup().then(() => {
|
||||
if (photos.photos) {
|
||||
setLocalPhotos(photos)
|
||||
}
|
||||
})
|
||||
}, [store])
|
||||
|
||||
const handleOpenCamera = useCallback(async () => {
|
||||
try {
|
||||
const cameraRes = await openCamera({
|
||||
const cameraRes = await openCamera(store, {
|
||||
mediaType: 'photo',
|
||||
cropping: true,
|
||||
...IMAGE_PARAMS,
|
||||
})
|
||||
const img = await compressIfNeeded(cameraRes, MAX_SIZE)
|
||||
|
@ -75,12 +85,13 @@ export const PhotoCarouselPicker = ({
|
|||
// ignore
|
||||
store.log.warn('Error using camera', err)
|
||||
}
|
||||
}, [store.log, selectedPhotos, onSelectPhotos])
|
||||
}, [store, selectedPhotos, onSelectPhotos])
|
||||
|
||||
const handleSelectPhoto = useCallback(
|
||||
async (item: PhotoIdentifier) => {
|
||||
try {
|
||||
const imgPath = await cropPhoto(
|
||||
store,
|
||||
item.node.image.uri,
|
||||
item.node.image.width,
|
||||
item.node.image.height,
|
||||
|
@ -91,11 +102,11 @@ export const PhotoCarouselPicker = ({
|
|||
store.log.warn('Error selecting photo', err)
|
||||
}
|
||||
},
|
||||
[store.log, selectedPhotos, onSelectPhotos],
|
||||
[store, selectedPhotos, onSelectPhotos],
|
||||
)
|
||||
|
||||
const handleOpenGallery = useCallback(() => {
|
||||
openPicker({
|
||||
openPicker(store, {
|
||||
multiple: true,
|
||||
maxFiles: 4 - selectedPhotos.length,
|
||||
mediaType: 'photo',
|
||||
|
@ -109,10 +120,10 @@ export const PhotoCarouselPicker = ({
|
|||
{width: image.width, height: image.height},
|
||||
{width: MAX_WIDTH, height: MAX_HEIGHT},
|
||||
)
|
||||
const cropperRes = await openCropper({
|
||||
const cropperRes = await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
path: image.path,
|
||||
...IMAGE_PARAMS,
|
||||
freeStyleCropEnabled: true,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
|
@ -121,7 +132,7 @@ export const PhotoCarouselPicker = ({
|
|||
}
|
||||
onSelectPhotos([...selectedPhotos, ...result])
|
||||
})
|
||||
}, [selectedPhotos, onSelectPhotos])
|
||||
}, [store, selectedPhotos, onSelectPhotos])
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
|
@ -150,15 +161,16 @@ export const PhotoCarouselPicker = ({
|
|||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{localPhotos.photos.map((item: PhotoIdentifier, index: number) => (
|
||||
<TouchableOpacity
|
||||
testID="openSelectPhotoButton"
|
||||
key={`local-image-${index}`}
|
||||
style={[pal.border, styles.photoButton]}
|
||||
onPress={() => handleSelectPhoto(item)}>
|
||||
<Image style={styles.photo} source={{uri: item.node.image.uri}} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
{localPhotos != null &&
|
||||
localPhotos.photos.map((item: PhotoIdentifier, index: number) => (
|
||||
<TouchableOpacity
|
||||
testID="openSelectPhotoButton"
|
||||
key={`local-image-${index}`}
|
||||
style={[pal.border, styles.photoButton]}
|
||||
onPress={() => handleSelectPhoto(item)}>
|
||||
<Image style={styles.photo} source={{uri: item.node.image.uri}} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import React, {useCallback} from 'react'
|
||||
import {StyleSheet, TouchableOpacity, ScrollView} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {
|
||||
openPicker,
|
||||
openCamera,
|
||||
openCropper,
|
||||
} from '../../util/images/image-crop-picker/ImageCropPicker'
|
||||
import {compressIfNeeded, scaleDownDimensions} from '../../../../lib/images'
|
||||
import {usePalette} from '../../../lib/hooks/usePalette'
|
||||
import {useStores, RootStoreModel} from '../../../../state'
|
||||
|
||||
const MAX_WIDTH = 1000
|
||||
const MAX_HEIGHT = 1000
|
||||
const MAX_SIZE = 300000
|
||||
|
||||
const IMAGE_PARAMS = {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
freeStyleCropEnabled: true,
|
||||
}
|
||||
|
||||
export async function cropPhoto(
|
||||
store: RootStoreModel,
|
||||
path: string,
|
||||
imgWidth = MAX_WIDTH,
|
||||
imgHeight = MAX_HEIGHT,
|
||||
) {
|
||||
// choose target dimensions based on the original
|
||||
// this causes the photo cropper to start with the full image "selected"
|
||||
const {width, height} = scaleDownDimensions(
|
||||
{width: imgWidth, height: imgHeight},
|
||||
{width: MAX_WIDTH, height: MAX_HEIGHT},
|
||||
)
|
||||
const cropperRes = await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
path,
|
||||
freeStyleCropEnabled: true,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
const img = await compressIfNeeded(cropperRes, MAX_SIZE)
|
||||
return img.path
|
||||
}
|
||||
|
||||
export const PhotoCarouselPicker = ({
|
||||
selectedPhotos,
|
||||
onSelectPhotos,
|
||||
}: {
|
||||
selectedPhotos: string[]
|
||||
onSelectPhotos: (v: string[]) => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
|
||||
const handleOpenCamera = useCallback(async () => {
|
||||
try {
|
||||
const cameraRes = await openCamera(store, {
|
||||
mediaType: 'photo',
|
||||
...IMAGE_PARAMS,
|
||||
})
|
||||
const img = await compressIfNeeded(cameraRes, MAX_SIZE)
|
||||
onSelectPhotos([...selectedPhotos, img.path])
|
||||
} catch (err: any) {
|
||||
// ignore
|
||||
store.log.warn('Error using camera', err)
|
||||
}
|
||||
}, [store, selectedPhotos, onSelectPhotos])
|
||||
|
||||
const handleOpenGallery = useCallback(() => {
|
||||
openPicker(store, {
|
||||
multiple: true,
|
||||
maxFiles: 4 - selectedPhotos.length,
|
||||
mediaType: 'photo',
|
||||
}).then(async items => {
|
||||
const result = []
|
||||
|
||||
for (const image of items) {
|
||||
// choose target dimensions based on the original
|
||||
// this causes the photo cropper to start with the full image "selected"
|
||||
const {width, height} = scaleDownDimensions(
|
||||
{width: image.width, height: image.height},
|
||||
{width: MAX_WIDTH, height: MAX_HEIGHT},
|
||||
)
|
||||
const cropperRes = await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
path: image.path,
|
||||
freeStyleCropEnabled: true,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE)
|
||||
result.push(finalImg.path)
|
||||
}
|
||||
onSelectPhotos([...selectedPhotos, ...result])
|
||||
})
|
||||
}, [store, selectedPhotos, onSelectPhotos])
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
testID="photoCarouselPickerView"
|
||||
horizontal
|
||||
style={[pal.view, styles.photosContainer]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
showsHorizontalScrollIndicator={false}>
|
||||
<TouchableOpacity
|
||||
testID="openCameraButton"
|
||||
style={[styles.galleryButton, pal.border, styles.photo]}
|
||||
onPress={handleOpenCamera}>
|
||||
<FontAwesomeIcon
|
||||
icon="camera"
|
||||
size={24}
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="openGalleryButton"
|
||||
style={[styles.galleryButton, pal.border, styles.photo]}
|
||||
onPress={handleOpenGallery}>
|
||||
<FontAwesomeIcon
|
||||
icon="image"
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
photosContainer: {
|
||||
width: '100%',
|
||||
maxHeight: 96,
|
||||
padding: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
galleryButton: {
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
photoButton: {
|
||||
width: 75,
|
||||
height: 75,
|
||||
marginRight: 8,
|
||||
borderWidth: 1,
|
||||
borderRadius: 16,
|
||||
},
|
||||
photo: {
|
||||
width: 75,
|
||||
height: 75,
|
||||
marginRight: 8,
|
||||
borderRadius: 16,
|
||||
},
|
||||
})
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {Image as PickedImage} from '../util/images/ImageCropPicker'
|
||||
import {PickedMedia} from '../util/images/image-crop-picker/ImageCropPicker'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {useStores} from '../../../state'
|
||||
|
@ -48,12 +48,12 @@ export function Component({
|
|||
const [userAvatar, setUserAvatar] = useState<string | undefined>(
|
||||
profileView.avatar,
|
||||
)
|
||||
const [newUserBanner, setNewUserBanner] = useState<PickedImage | undefined>()
|
||||
const [newUserAvatar, setNewUserAvatar] = useState<PickedImage | undefined>()
|
||||
const [newUserBanner, setNewUserBanner] = useState<PickedMedia | undefined>()
|
||||
const [newUserAvatar, setNewUserAvatar] = useState<PickedMedia | undefined>()
|
||||
const onPressCancel = () => {
|
||||
store.shell.closeModal()
|
||||
}
|
||||
const onSelectNewAvatar = async (img: PickedImage) => {
|
||||
const onSelectNewAvatar = async (img: PickedMedia) => {
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 300000)
|
||||
setNewUserAvatar(finalImg)
|
||||
|
@ -62,7 +62,7 @@ export function Component({
|
|||
setError(e.message || e.toString())
|
||||
}
|
||||
}
|
||||
const onSelectNewBanner = async (img: PickedImage) => {
|
||||
const onSelectNewBanner = async (img: PickedMedia) => {
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 500000)
|
||||
setNewUserBanner(finalImg)
|
||||
|
|
|
@ -11,6 +11,7 @@ import * as EditProfileModal from './EditProfile'
|
|||
import * as ServerInputModal from './ServerInput'
|
||||
import * as ReportPostModal from './ReportPost'
|
||||
import * as ReportAccountModal from './ReportAccount'
|
||||
import * as CropImageModal from './crop-image/CropImage.web'
|
||||
|
||||
export const Modal = observer(function Modal() {
|
||||
const store = useStores()
|
||||
|
@ -50,6 +51,12 @@ export const Modal = observer(function Modal() {
|
|||
element = <ReportPostModal.Component />
|
||||
} else if (store.shell.activeModal?.name === 'report-account') {
|
||||
element = <ReportAccountModal.Component />
|
||||
} else if (store.shell.activeModal?.name === 'crop-image') {
|
||||
element = (
|
||||
<CropImageModal.Component
|
||||
{...(store.shell.activeModal as models.CropImageModal)}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* NOTE
|
||||
* This modal is used only in the web build
|
||||
* Native uses a third-party library
|
||||
*/
|
||||
|
||||
export const snapPoints = ['0%']
|
||||
|
||||
export function Component() {
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import ImageEditor from 'react-avatar-editor'
|
||||
import {Slider} from '@miblanchard/react-native-slider'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {PickedMedia} from '../../util/images/image-crop-picker/types'
|
||||
import {s, gradients} from '../../../lib/styles'
|
||||
import {useStores} from '../../../../state'
|
||||
import {usePalette} from '../../../lib/hooks/usePalette'
|
||||
import {SquareIcon, RectWideIcon, RectTallIcon} from '../../../lib/icons'
|
||||
|
||||
enum AspectRatio {
|
||||
Square = 'square',
|
||||
Wide = 'wide',
|
||||
Tall = 'tall',
|
||||
}
|
||||
interface Dim {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
const DIMS: Record<string, Dim> = {
|
||||
[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,
|
||||
onSelect,
|
||||
}: {
|
||||
uri: string
|
||||
onSelect: (img?: PickedMedia) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
|
||||
const [scale, setScale] = React.useState<number>(1)
|
||||
|
||||
const doSetAs = (v: AspectRatio) => () => setAs(v)
|
||||
|
||||
const onPressCancel = () => {
|
||||
onSelect(undefined)
|
||||
store.shell.closeModal()
|
||||
}
|
||||
const onPressDone = () => {
|
||||
console.log('TODO')
|
||||
onSelect(undefined) // TODO
|
||||
store.shell.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
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
<View style={[styles.cropper, cropperStyle]}>
|
||||
<ImageEditor
|
||||
style={styles.imageEditor}
|
||||
image={uri}
|
||||
width={DIMS[as].width}
|
||||
height={DIMS[as].height}
|
||||
scale={scale}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
<TouchableOpacity onPress={doSetAs(AspectRatio.Wide)}>
|
||||
<RectWideIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Wide ? s.blue3 : undefined}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={doSetAs(AspectRatio.Tall)}>
|
||||
<RectTallIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Tall ? s.blue3 : undefined}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={doSetAs(AspectRatio.Square)}>
|
||||
<SquareIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Square ? s.blue3 : undefined}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.btns}>
|
||||
<TouchableOpacity onPress={onPressCancel}>
|
||||
<Text type="xl" style={pal.link}>
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity onPress={onPressDone}>
|
||||
<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}>
|
||||
Done
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cropper: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
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,
|
||||
},
|
||||
})
|
|
@ -6,8 +6,9 @@ import {
|
|||
openCamera,
|
||||
openCropper,
|
||||
openPicker,
|
||||
Image as PickedImage,
|
||||
} from './images/ImageCropPicker'
|
||||
PickedMedia,
|
||||
} from './images/image-crop-picker/ImageCropPicker'
|
||||
import {useStores} from '../../../state'
|
||||
import {colors, gradients} from '../../lib/styles'
|
||||
|
||||
export function UserAvatar({
|
||||
|
@ -21,8 +22,9 @@ export function UserAvatar({
|
|||
handle: string
|
||||
displayName: string | undefined
|
||||
avatar?: string | null
|
||||
onSelectNewAvatar?: (img: PickedImage) => void
|
||||
onSelectNewAvatar?: (img: PickedMedia) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const initials = getInitials(displayName || handle)
|
||||
|
||||
const handleEditAvatar = useCallback(() => {
|
||||
|
@ -30,37 +32,32 @@ export function UserAvatar({
|
|||
{
|
||||
text: 'Take a new photo',
|
||||
onPress: () => {
|
||||
openCamera({
|
||||
openCamera(store, {
|
||||
mediaType: 'photo',
|
||||
cropping: true,
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
cropperCircleOverlay: true,
|
||||
forceJpg: true, // ios only
|
||||
compressImageQuality: 1,
|
||||
}).then(onSelectNewAvatar)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Select from gallery',
|
||||
onPress: () => {
|
||||
openPicker({
|
||||
openPicker(store, {
|
||||
mediaType: 'photo',
|
||||
}).then(async item => {
|
||||
await openCropper({
|
||||
}).then(async items => {
|
||||
await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
path: item.path,
|
||||
path: items[0].path,
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
cropperCircleOverlay: true,
|
||||
forceJpg: true, // ios only
|
||||
compressImageQuality: 1,
|
||||
}).then(onSelectNewAvatar)
|
||||
})
|
||||
},
|
||||
},
|
||||
])
|
||||
}, [onSelectNewAvatar])
|
||||
}, [store, onSelectNewAvatar])
|
||||
|
||||
const renderSvg = (svgSize: number, svgInitials: string) => (
|
||||
<Svg width={svgSize} height={svgSize} viewBox="0 0 100 100">
|
||||
|
|
|
@ -2,57 +2,56 @@ import React, {useCallback} from 'react'
|
|||
import {StyleSheet, View, TouchableOpacity, Alert, Image} from 'react-native'
|
||||
import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Image as PickedImage} from './images/ImageCropPicker'
|
||||
import {colors, gradients} from '../../lib/styles'
|
||||
import {openCamera, openCropper, openPicker} from './images/ImageCropPicker'
|
||||
import {
|
||||
openCamera,
|
||||
openCropper,
|
||||
openPicker,
|
||||
PickedMedia,
|
||||
} from './images/image-crop-picker/ImageCropPicker'
|
||||
import {useStores} from '../../../state'
|
||||
|
||||
export function UserBanner({
|
||||
banner,
|
||||
onSelectNewBanner,
|
||||
}: {
|
||||
banner?: string | null
|
||||
onSelectNewBanner?: (img: PickedImage) => void
|
||||
onSelectNewBanner?: (img: PickedMedia) => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const handleEditBanner = useCallback(() => {
|
||||
Alert.alert('Select upload method', '', [
|
||||
{
|
||||
text: 'Take a new photo',
|
||||
onPress: () => {
|
||||
openCamera({
|
||||
openCamera(store, {
|
||||
mediaType: 'photo',
|
||||
cropping: true,
|
||||
compressImageMaxWidth: 3000,
|
||||
// compressImageMaxWidth: 3000, TODO needed?
|
||||
width: 3000,
|
||||
compressImageMaxHeight: 1000,
|
||||
// compressImageMaxHeight: 1000, TODO needed?
|
||||
height: 1000,
|
||||
forceJpg: true, // ios only
|
||||
compressImageQuality: 1,
|
||||
includeExif: true,
|
||||
}).then(onSelectNewBanner)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Select from gallery',
|
||||
onPress: () => {
|
||||
openPicker({
|
||||
openPicker(store, {
|
||||
mediaType: 'photo',
|
||||
}).then(async item => {
|
||||
await openCropper({
|
||||
}).then(async items => {
|
||||
await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
path: item.path,
|
||||
compressImageMaxWidth: 3000,
|
||||
path: items[0].path,
|
||||
// compressImageMaxWidth: 3000, TODO needed?
|
||||
width: 3000,
|
||||
compressImageMaxHeight: 1000,
|
||||
// compressImageMaxHeight: 1000, TODO needed?
|
||||
height: 1000,
|
||||
forceJpg: true, // ios only
|
||||
compressImageQuality: 1,
|
||||
includeExif: true,
|
||||
}).then(onSelectNewBanner)
|
||||
})
|
||||
},
|
||||
},
|
||||
])
|
||||
}, [onSelectNewBanner])
|
||||
}, [store, onSelectNewBanner])
|
||||
|
||||
const renderSvg = () => (
|
||||
<Svg width="100%" height="150" viewBox="50 0 200 100">
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
export {
|
||||
openPicker,
|
||||
openCamera,
|
||||
openCropper,
|
||||
} from 'react-native-image-crop-picker'
|
||||
export type {Image} from 'react-native-image-crop-picker'
|
|
@ -1,32 +0,0 @@
|
|||
import type {
|
||||
Image,
|
||||
Video,
|
||||
ImageOrVideo,
|
||||
Options,
|
||||
PossibleArray,
|
||||
} from 'react-native-image-crop-picker'
|
||||
|
||||
export type {Image} from 'react-native-image-crop-picker'
|
||||
|
||||
type MediaType<O> = O extends {mediaType: 'photo'}
|
||||
? Image
|
||||
: O extends {mediaType: 'video'}
|
||||
? Video
|
||||
: ImageOrVideo
|
||||
|
||||
export async function openPicker<O extends Options>(
|
||||
_options: O,
|
||||
): Promise<PossibleArray<O, MediaType<O>>> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
||||
export async function openCamera<O extends Options>(
|
||||
_options: O,
|
||||
): Promise<PossibleArray<O, MediaType<O>>> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
||||
export async function openCropper(_options: Options): Promise<Image> {
|
||||
// TODO
|
||||
throw new Error('TODO')
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import {
|
||||
openPicker as openPickerFn,
|
||||
openCamera as openCameraFn,
|
||||
openCropper as openCropperFn,
|
||||
ImageOrVideo,
|
||||
} from 'react-native-image-crop-picker'
|
||||
import {RootStoreModel} from '../../../../../state'
|
||||
import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
|
||||
export type {PickedMedia} from './types'
|
||||
|
||||
/**
|
||||
* NOTE
|
||||
* These methods all include the RootStoreModel as the first param
|
||||
* because the web versions require it. The signatures have to remain
|
||||
* equivalent between the different forms, but the store param is not
|
||||
* used here.
|
||||
* -prf
|
||||
*/
|
||||
|
||||
export async function openPicker(
|
||||
_store: RootStoreModel,
|
||||
opts: PickerOpts,
|
||||
): Promise<PickedMedia[]> {
|
||||
const mediaType = opts.mediaType || 'photo'
|
||||
const items = await openPickerFn({
|
||||
mediaType,
|
||||
multiple: opts.multiple,
|
||||
maxFiles: opts.maxFiles,
|
||||
})
|
||||
const toMedia = (item: ImageOrVideo) => ({
|
||||
mediaType,
|
||||
path: item.path,
|
||||
mime: item.mime,
|
||||
size: item.size,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
})
|
||||
if (Array.isArray(items)) {
|
||||
return items.map(toMedia)
|
||||
}
|
||||
return [toMedia(items)]
|
||||
}
|
||||
|
||||
export async function openCamera(
|
||||
_store: RootStoreModel,
|
||||
opts: CameraOpts,
|
||||
): Promise<PickedMedia> {
|
||||
const mediaType = opts.mediaType || 'photo'
|
||||
const item = await openCameraFn({
|
||||
mediaType,
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
freeStyleCropEnabled: opts.freeStyleCropEnabled,
|
||||
cropperCircleOverlay: opts.cropperCircleOverlay,
|
||||
cropping: true,
|
||||
forceJpg: true, // ios only
|
||||
compressImageQuality: 1.0,
|
||||
})
|
||||
return {
|
||||
mediaType,
|
||||
path: item.path,
|
||||
mime: item.mime,
|
||||
size: item.size,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
}
|
||||
}
|
||||
|
||||
export async function openCropper(
|
||||
_store: RootStoreModel,
|
||||
opts: CropperOpts,
|
||||
): Promise<PickedMedia> {
|
||||
const mediaType = opts.mediaType || 'photo'
|
||||
const item = await openCropperFn({
|
||||
path: opts.path,
|
||||
mediaType: opts.mediaType || 'photo',
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
freeStyleCropEnabled: opts.freeStyleCropEnabled,
|
||||
cropperCircleOverlay: opts.cropperCircleOverlay,
|
||||
forceJpg: true, // ios only
|
||||
compressImageQuality: 1.0,
|
||||
})
|
||||
return {
|
||||
mediaType,
|
||||
path: item.path,
|
||||
mime: item.mime,
|
||||
size: item.size,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/// <reference lib="dom" />
|
||||
|
||||
import {CropImageModal} from '../../../../../state/models/shell-ui'
|
||||
import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
|
||||
export type {PickedMedia} from './types'
|
||||
import {RootStoreModel} from '../../../../../state'
|
||||
|
||||
interface PickedFile {
|
||||
uri: string
|
||||
path: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export async function openPicker(
|
||||
store: RootStoreModel,
|
||||
opts: PickerOpts,
|
||||
): Promise<PickedMedia[] | PickedMedia> {
|
||||
const res = await selectFile(opts)
|
||||
return new Promise((resolve, reject) => {
|
||||
store.shell.openModal(
|
||||
new CropImageModal(res.uri, (img?: PickedMedia) => {
|
||||
if (img) {
|
||||
resolve(img)
|
||||
} else {
|
||||
reject(new Error('Canceled'))
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export async function openCamera(
|
||||
_store: RootStoreModel,
|
||||
opts: CameraOpts,
|
||||
): Promise<PickedMedia> {
|
||||
const mediaType = opts.mediaType || 'photo'
|
||||
throw new Error('TODO')
|
||||
}
|
||||
|
||||
export async function openCropper(
|
||||
_store: RootStoreModel,
|
||||
opts: CropperOpts,
|
||||
): Promise<PickedMedia> {
|
||||
const mediaType = opts.mediaType || 'photo'
|
||||
throw new Error('TODO')
|
||||
}
|
||||
|
||||
function selectFile(opts: PickerOpts): Promise<PickedFile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
var input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = opts.mediaType === 'photo' ? 'image/*' : '*/*'
|
||||
input.onchange = e => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const file = target?.files?.[0]
|
||||
if (!file) {
|
||||
return reject(new Error('Canceled'))
|
||||
}
|
||||
|
||||
var reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = readerEvent => {
|
||||
if (!readerEvent.target) {
|
||||
return reject(new Error('Canceled'))
|
||||
}
|
||||
resolve({
|
||||
uri: readerEvent.target.result as string,
|
||||
path: file.name,
|
||||
size: file.size,
|
||||
})
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
})
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
export interface PickerOpts {
|
||||
mediaType?: 'photo'
|
||||
multiple?: boolean
|
||||
maxFiles?: number
|
||||
}
|
||||
|
||||
export interface CameraOpts {
|
||||
mediaType?: 'photo'
|
||||
width: number
|
||||
height: number
|
||||
freeStyleCropEnabled?: boolean
|
||||
cropperCircleOverlay?: boolean
|
||||
}
|
||||
|
||||
export interface CropperOpts {
|
||||
path: string
|
||||
mediaType?: 'photo'
|
||||
width: number
|
||||
height: number
|
||||
freeStyleCropEnabled?: boolean
|
||||
cropperCircleOverlay?: boolean
|
||||
}
|
||||
|
||||
export interface PickedMedia {
|
||||
mediaType: 'photo'
|
||||
path: string
|
||||
mime: string
|
||||
size: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
|
||||
import Svg, {Path} from 'react-native-svg'
|
||||
import Svg, {Path, Rect} from 'react-native-svg'
|
||||
|
||||
export function GridIcon({
|
||||
style,
|
||||
|
@ -458,3 +458,72 @@ export function CommentBottomArrow({
|
|||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SquareIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.3,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth || 1}
|
||||
stroke="currentColor"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Rect x="6" y="6" width="12" height="12" strokeLinejoin="round" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function RectWideIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.3,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth || 1}
|
||||
stroke="currentColor"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Rect x="4" y="6" width="16" height="12" strokeLinejoin="round" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function RectTallIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.3,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth || 1}
|
||||
stroke="currentColor"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Rect x="6" y="4" width="12" height="16" strokeLinejoin="round" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -1065,7 +1065,7 @@
|
|||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.18.6"
|
||||
|
||||
"@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.16.4":
|
||||
"@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.12.1", "@babel/plugin-transform-runtime@^7.16.4":
|
||||
version "7.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz#9d2a9dbf4e12644d6f46e5e75bfbf02b5d6e9194"
|
||||
integrity sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==
|
||||
|
@ -2088,6 +2088,11 @@
|
|||
dependencies:
|
||||
semver "7.3.8"
|
||||
|
||||
"@miblanchard/react-native-slider@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@miblanchard/react-native-slider/-/react-native-slider-2.2.0.tgz#5d03cf49516ad0a3b4011fbcad53cb379800832b"
|
||||
integrity sha512-LepVGFVy6KtDVgMRIAAJJKQCXbcADkzK2R61t3LkD+IF2wG1J4I4KVo99GAsvU0EBKeCsjHPkR+6LGnB6xGzVA==
|
||||
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
|
||||
version "5.1.1-v1"
|
||||
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
|
||||
|
@ -2900,6 +2905,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-avatar-editor@^13.0.0":
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-avatar-editor/-/react-avatar-editor-13.0.0.tgz#5963e16c931746c47e478d669dd72d388b427393"
|
||||
integrity sha512-5ymOayy6mfT35xTqzni7UjXvCNEg8/pH4pI5RenITp9PBc02KGTYjSV1WboXiQDYSh5KomLT0ngBLEAIhV1QoQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-native@^0.67.3":
|
||||
version "0.67.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.67.17.tgz#afebc3fff1d6314840c13b7936e17fa350eb7aae"
|
||||
|
@ -2914,7 +2926,7 @@
|
|||
dependencies:
|
||||
"@types/react" "^17"
|
||||
|
||||
"@types/react@^17":
|
||||
"@types/react@*", "@types/react@^17":
|
||||
version "17.0.52"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.52.tgz#10d8b907b5c563ac014a541f289ae8eaa9bf2e9b"
|
||||
integrity sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==
|
||||
|
@ -11162,6 +11174,15 @@ react-app-polyfill@^3.0.0:
|
|||
regenerator-runtime "^0.13.9"
|
||||
whatwg-fetch "^3.6.2"
|
||||
|
||||
react-avatar-editor@^13.0.0:
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-13.0.0.tgz#55013625ee9ae715c1fe2dc553b8079994d8a5f2"
|
||||
integrity sha512-0xw63MbRRQdDy7YI1IXU9+7tTFxYEFLV8CABvryYOGjZmXRTH2/UA0mafe57ns62uaEFX181kA4XlGlxCaeXKA==
|
||||
dependencies:
|
||||
"@babel/plugin-transform-runtime" "^7.12.1"
|
||||
"@babel/runtime" "^7.12.5"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-circular-progressbar@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz#99e5ae499c21de82223b498289e96f66adb8fa3a"
|
||||
|
|
Loading…
Reference in New Issue