Implement image uploading in the web composer
This commit is contained in:
parent
0f293ecf95
commit
4182edfd7e
19 changed files with 338 additions and 281 deletions
|
@ -36,11 +36,18 @@ import {s, colors, gradients} from 'lib/styles'
|
|||
import {cleanError} from 'lib/strings/errors'
|
||||
import {detectLinkables, extractEntities} from 'lib/strings/rich-text-detection'
|
||||
import {getLinkMeta} from 'lib/link-meta/link-meta'
|
||||
import {downloadAndResize} from 'lib/images'
|
||||
import {PhotoCarouselPicker, cropPhoto} from './photos/PhotoCarouselPicker'
|
||||
import {getImageDim, downloadAndResize} from 'lib/media/manip'
|
||||
import {PhotoCarouselPicker} from './photos/PhotoCarouselPicker'
|
||||
import {cropAndCompressFlow, pickImagesFlow} from '../../../lib/media/picker'
|
||||
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
||||
import {SelectedPhoto} from './SelectedPhoto'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {
|
||||
POST_IMG_MAX_WIDTH,
|
||||
POST_IMG_MAX_HEIGHT,
|
||||
POST_IMG_MAX_SIZE,
|
||||
} from 'lib/constants'
|
||||
import {isWeb} from 'platform/detection'
|
||||
|
||||
const MAX_TEXT_LENGTH = 256
|
||||
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
|
||||
|
@ -61,7 +68,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
onPost?: ComposerOpts['onPost']
|
||||
onClose: () => void
|
||||
}) {
|
||||
const {track, screen} = useAnalytics()
|
||||
const {track} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const textInput = useRef<TextInputRef>(null)
|
||||
|
@ -174,12 +181,24 @@ export const ComposePost = observer(function ComposePost({
|
|||
const onPressContainer = () => {
|
||||
textInput.current?.focus()
|
||||
}
|
||||
const onPressSelectPhotos = () => {
|
||||
const onPressSelectPhotos = async () => {
|
||||
track('ComposePost:SelectPhotos')
|
||||
if (isSelectingPhotos) {
|
||||
setIsSelectingPhotos(false)
|
||||
} else if (selectedPhotos.length < 4) {
|
||||
setIsSelectingPhotos(true)
|
||||
if (isWeb) {
|
||||
if (selectedPhotos.length < 4) {
|
||||
const images = await pickImagesFlow(
|
||||
store,
|
||||
4 - selectedPhotos.length,
|
||||
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
||||
POST_IMG_MAX_SIZE,
|
||||
)
|
||||
setSelectedPhotos([...selectedPhotos, ...images])
|
||||
}
|
||||
} else {
|
||||
if (isSelectingPhotos) {
|
||||
setIsSelectingPhotos(false)
|
||||
} else if (selectedPhotos.length < 4) {
|
||||
setIsSelectingPhotos(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
const onSelectPhotos = (photos: string[]) => {
|
||||
|
@ -220,7 +239,19 @@ export const ComposePost = observer(function ComposePost({
|
|||
}
|
||||
const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri))
|
||||
if (imgUri) {
|
||||
const finalImgPath = await cropPhoto(store, imgUri)
|
||||
let imgDim
|
||||
try {
|
||||
imgDim = await getImageDim(imgUri)
|
||||
} catch (e) {
|
||||
imgDim = {width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT}
|
||||
}
|
||||
const finalImgPath = await cropAndCompressFlow(
|
||||
store,
|
||||
imgUri,
|
||||
imgDim,
|
||||
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
||||
POST_IMG_MAX_SIZE,
|
||||
)
|
||||
onSelectPhotos([...selectedPhotos, finalImgPath])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,57 +9,24 @@ import {
|
|||
openPicker,
|
||||
openCamera,
|
||||
openCropper,
|
||||
} from '../../util/images/image-crop-picker/ImageCropPicker'
|
||||
cropAndCompressFlow,
|
||||
} from '../../../../lib/media/picker'
|
||||
import {
|
||||
UserLocalPhotosModel,
|
||||
PhotoIdentifier,
|
||||
} from 'state/models/user-local-photos'
|
||||
import {
|
||||
compressIfNeeded,
|
||||
moveToPremanantPath,
|
||||
scaleDownDimensions,
|
||||
} from 'lib/images'
|
||||
import {compressIfNeeded} from 'lib/media/manip'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useStores, RootStoreModel} from 'state/index'
|
||||
import {useStores} from 'state/index'
|
||||
import {
|
||||
requestPhotoAccessIfNeeded,
|
||||
requestCameraAccessIfNeeded,
|
||||
} from 'lib/permissions'
|
||||
|
||||
const MAX_WIDTH = 2000
|
||||
const MAX_HEIGHT = 2000
|
||||
const MAX_SIZE = 1000000
|
||||
|
||||
const IMAGE_PARAMS = {
|
||||
width: 2000,
|
||||
height: 2000,
|
||||
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)
|
||||
const permanentPath = await moveToPremanantPath(img.path)
|
||||
return permanentPath
|
||||
}
|
||||
import {
|
||||
POST_IMG_MAX_WIDTH,
|
||||
POST_IMG_MAX_HEIGHT,
|
||||
POST_IMG_MAX_SIZE,
|
||||
} from 'lib/constants'
|
||||
|
||||
export const PhotoCarouselPicker = ({
|
||||
selectedPhotos,
|
||||
|
@ -92,9 +59,11 @@ export const PhotoCarouselPicker = ({
|
|||
}
|
||||
const cameraRes = await openCamera(store, {
|
||||
mediaType: 'photo',
|
||||
...IMAGE_PARAMS,
|
||||
width: POST_IMG_MAX_WIDTH,
|
||||
height: POST_IMG_MAX_HEIGHT,
|
||||
freeStyleCropEnabled: true,
|
||||
})
|
||||
const img = await compressIfNeeded(cameraRes, MAX_SIZE)
|
||||
const img = await compressIfNeeded(cameraRes, POST_IMG_MAX_SIZE)
|
||||
onSelectPhotos([...selectedPhotos, img.path])
|
||||
} catch (err: any) {
|
||||
// ignore
|
||||
|
@ -106,11 +75,15 @@ export const PhotoCarouselPicker = ({
|
|||
async (item: PhotoIdentifier) => {
|
||||
track('PhotoCarouselPicker:PhotoSelected')
|
||||
try {
|
||||
const imgPath = await cropPhoto(
|
||||
const imgPath = await cropAndCompressFlow(
|
||||
store,
|
||||
item.node.image.uri,
|
||||
item.node.image.width,
|
||||
item.node.image.height,
|
||||
{
|
||||
width: item.node.image.width,
|
||||
height: item.node.image.height,
|
||||
},
|
||||
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
||||
POST_IMG_MAX_SIZE,
|
||||
)
|
||||
onSelectPhotos([...selectedPhotos, imgPath])
|
||||
} catch (err: any) {
|
||||
|
@ -132,24 +105,16 @@ export const PhotoCarouselPicker = ({
|
|||
mediaType: 'photo',
|
||||
})
|
||||
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},
|
||||
result.push(
|
||||
await cropAndCompressFlow(
|
||||
store,
|
||||
image.path,
|
||||
image,
|
||||
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
||||
POST_IMG_MAX_SIZE,
|
||||
),
|
||||
)
|
||||
const cropperRes = await openCropper(store, {
|
||||
mediaType: 'photo',
|
||||
path: image.path,
|
||||
...IMAGE_PARAMS,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE)
|
||||
const permanentPath = await moveToPremanantPath(finalImg.path)
|
||||
result.push(permanentPath)
|
||||
}
|
||||
onSelectPhotos([...selectedPhotos, ...result])
|
||||
}, [track, store, selectedPhotos, onSelectPhotos])
|
||||
|
|
|
@ -1,158 +1,10 @@
|
|||
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/index'
|
||||
import React from 'react'
|
||||
|
||||
const MAX_WIDTH = 1000
|
||||
const MAX_HEIGHT = 1000
|
||||
const MAX_SIZE = 300000
|
||||
// Not used on Web
|
||||
|
||||
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,
|
||||
}: {
|
||||
export const PhotoCarouselPicker = (_opts: {
|
||||
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>
|
||||
)
|
||||
return <></>
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue