Break out the web/native image picking code and make some progress on the web version

This commit is contained in:
Paul Frazee 2023-01-27 15:51:24 -06:00
parent 0673129b20
commit 7916b26aad
21 changed files with 738 additions and 138 deletions

View file

@ -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"

View file

@ -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>
)
}

View file

@ -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,
},
})