diff options
Diffstat (limited to 'src/view/com/composer/photos')
-rw-r--r-- | src/view/com/composer/photos/OpenCameraBtn.tsx | 84 | ||||
-rw-r--r-- | src/view/com/composer/photos/PhotoCarouselPicker.tsx | 187 | ||||
-rw-r--r-- | src/view/com/composer/photos/PhotoCarouselPicker.web.tsx | 10 | ||||
-rw-r--r-- | src/view/com/composer/photos/SelectPhotoBtn.tsx | 94 | ||||
-rw-r--r-- | src/view/com/composer/photos/SelectedPhotos.tsx | 96 |
5 files changed, 274 insertions, 197 deletions
diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx new file mode 100644 index 000000000..cf4a4c7d1 --- /dev/null +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import {TouchableOpacity} from 'react-native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' +import {useStores} from 'state/index' +import {s} from 'lib/styles' +import {isDesktopWeb} from 'platform/detection' +import {openCamera} from 'lib/media/picker' +import {compressIfNeeded} from 'lib/media/manip' +import {useCameraPermission} from 'lib/hooks/usePermissions' +import { + POST_IMG_MAX_WIDTH, + POST_IMG_MAX_HEIGHT, + POST_IMG_MAX_SIZE, +} from 'lib/constants' + +const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} + +export function OpenCameraBtn({ + enabled, + selectedPhotos, + onSelectPhotos, +}: { + enabled: boolean + selectedPhotos: string[] + onSelectPhotos: (v: string[]) => void +}) { + const pal = usePalette('default') + const {track} = useAnalytics() + const store = useStores() + const {requestCameraAccessIfNeeded} = useCameraPermission() + + const onPressTakePicture = React.useCallback(async () => { + track('Composer:CameraOpened') + if (!enabled) { + return + } + try { + if (!(await requestCameraAccessIfNeeded())) { + return + } + const cameraRes = await openCamera(store, { + mediaType: 'photo', + width: POST_IMG_MAX_WIDTH, + height: POST_IMG_MAX_HEIGHT, + freeStyleCropEnabled: true, + }) + const img = await compressIfNeeded(cameraRes, POST_IMG_MAX_SIZE) + onSelectPhotos([...selectedPhotos, img.path]) + } catch (err: any) { + // ignore + store.log.warn('Error using camera', err) + } + }, [ + track, + store, + onSelectPhotos, + selectedPhotos, + enabled, + requestCameraAccessIfNeeded, + ]) + + if (isDesktopWeb) { + return <></> + } + + return ( + <TouchableOpacity + testID="openCameraButton" + onPress={onPressTakePicture} + style={[s.pl5]} + hitSlop={HITSLOP}> + <FontAwesomeIcon + icon="camera" + style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle} + size={24} + /> + </TouchableOpacity> + ) +} diff --git a/src/view/com/composer/photos/PhotoCarouselPicker.tsx b/src/view/com/composer/photos/PhotoCarouselPicker.tsx deleted file mode 100644 index 580e9746e..000000000 --- a/src/view/com/composer/photos/PhotoCarouselPicker.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React, {useCallback} from 'react' -import {Image, StyleSheet, TouchableOpacity, ScrollView} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {useAnalytics} from 'lib/analytics' -import { - openPicker, - openCamera, - cropAndCompressFlow, -} from '../../../../lib/media/picker' -import { - UserLocalPhotosModel, - PhotoIdentifier, -} from 'state/models/user-local-photos' -import {compressIfNeeded} from 'lib/media/manip' -import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' -import { - requestPhotoAccessIfNeeded, - requestCameraAccessIfNeeded, -} from 'lib/permissions' -import { - POST_IMG_MAX_WIDTH, - POST_IMG_MAX_HEIGHT, - POST_IMG_MAX_SIZE, -} from 'lib/constants' - -export const PhotoCarouselPicker = ({ - selectedPhotos, - onSelectPhotos, -}: { - selectedPhotos: string[] - onSelectPhotos: (v: string[]) => void -}) => { - const {track} = useAnalytics() - const pal = usePalette('default') - const store = useStores() - const [isSetup, setIsSetup] = React.useState<boolean>(false) - - const localPhotos = React.useMemo<UserLocalPhotosModel>( - () => new UserLocalPhotosModel(store), - [store], - ) - - React.useEffect(() => { - // initial setup - localPhotos.setup().then(() => { - setIsSetup(true) - }) - }, [localPhotos]) - - const handleOpenCamera = useCallback(async () => { - try { - if (!(await requestCameraAccessIfNeeded())) { - return - } - const cameraRes = await openCamera(store, { - mediaType: 'photo', - width: POST_IMG_MAX_WIDTH, - height: POST_IMG_MAX_HEIGHT, - freeStyleCropEnabled: true, - }) - const img = await compressIfNeeded(cameraRes, POST_IMG_MAX_SIZE) - onSelectPhotos([...selectedPhotos, img.path]) - } catch (err: any) { - // ignore - store.log.warn('Error using camera', err) - } - }, [store, selectedPhotos, onSelectPhotos]) - - const handleSelectPhoto = useCallback( - async (item: PhotoIdentifier) => { - track('PhotoCarouselPicker:PhotoSelected') - try { - const imgPath = await cropAndCompressFlow( - store, - item.node.image.uri, - { - 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) { - // ignore - store.log.warn('Error selecting photo', err) - } - }, - [track, store, onSelectPhotos, selectedPhotos], - ) - - const handleOpenGallery = useCallback(async () => { - track('PhotoCarouselPicker:GalleryOpened') - if (!(await requestPhotoAccessIfNeeded())) { - return - } - const items = await openPicker(store, { - multiple: true, - maxFiles: 4 - selectedPhotos.length, - mediaType: 'photo', - }) - const result = [] - for (const image of items) { - result.push( - await cropAndCompressFlow( - store, - image.path, - image, - {width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT}, - POST_IMG_MAX_SIZE, - ), - ) - } - onSelectPhotos([...selectedPhotos, ...result]) - }, [track, 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> - {isSetup && - 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> - ) -} - -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, - }, -}) diff --git a/src/view/com/composer/photos/PhotoCarouselPicker.web.tsx b/src/view/com/composer/photos/PhotoCarouselPicker.web.tsx deleted file mode 100644 index ff4350b0c..000000000 --- a/src/view/com/composer/photos/PhotoCarouselPicker.web.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' - -// Not used on Web - -export const PhotoCarouselPicker = (_opts: { - selectedPhotos: string[] - onSelectPhotos: (v: string[]) => void -}) => { - return <></> -} diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx new file mode 100644 index 000000000..bdcb0534a --- /dev/null +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -0,0 +1,94 @@ +import React from 'react' +import {TouchableOpacity} from 'react-native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' +import {useStores} from 'state/index' +import {s} from 'lib/styles' +import {isDesktopWeb} from 'platform/detection' +import {openPicker, cropAndCompressFlow, pickImagesFlow} from 'lib/media/picker' +import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' +import { + POST_IMG_MAX_WIDTH, + POST_IMG_MAX_HEIGHT, + POST_IMG_MAX_SIZE, +} from 'lib/constants' + +const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} + +export function SelectPhotoBtn({ + enabled, + selectedPhotos, + onSelectPhotos, +}: { + enabled: boolean + selectedPhotos: string[] + onSelectPhotos: (v: string[]) => void +}) { + const pal = usePalette('default') + const {track} = useAnalytics() + const store = useStores() + const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() + + const onPressSelectPhotos = React.useCallback(async () => { + track('Composer:GalleryOpened') + if (!enabled) { + return + } + if (isDesktopWeb) { + const images = await pickImagesFlow( + store, + 4 - selectedPhotos.length, + {width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT}, + POST_IMG_MAX_SIZE, + ) + onSelectPhotos([...selectedPhotos, ...images]) + } else { + if (!(await requestPhotoAccessIfNeeded())) { + return + } + const items = await openPicker(store, { + multiple: true, + maxFiles: 4 - selectedPhotos.length, + mediaType: 'photo', + }) + const result = [] + for (const image of items) { + result.push( + await cropAndCompressFlow( + store, + image.path, + image, + {width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT}, + POST_IMG_MAX_SIZE, + ), + ) + } + onSelectPhotos([...selectedPhotos, ...result]) + } + }, [ + track, + store, + onSelectPhotos, + selectedPhotos, + enabled, + requestPhotoAccessIfNeeded, + ]) + + return ( + <TouchableOpacity + testID="openGalleryBtn" + onPress={onPressSelectPhotos} + style={[s.pl5, s.pr20]} + hitSlop={HITSLOP}> + <FontAwesomeIcon + icon={['far', 'image']} + style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle} + size={24} + /> + </TouchableOpacity> + ) +} diff --git a/src/view/com/composer/photos/SelectedPhotos.tsx b/src/view/com/composer/photos/SelectedPhotos.tsx new file mode 100644 index 000000000..c2a00ce53 --- /dev/null +++ b/src/view/com/composer/photos/SelectedPhotos.tsx @@ -0,0 +1,96 @@ +import React, {useCallback} from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import Image from 'view/com/util/images/Image' +import {colors} from 'lib/styles' + +export const SelectedPhotos = ({ + selectedPhotos, + onSelectPhotos, +}: { + selectedPhotos: string[] + onSelectPhotos: (v: string[]) => void +}) => { + const imageStyle = + selectedPhotos.length === 1 + ? styles.image250 + : selectedPhotos.length === 2 + ? styles.image175 + : styles.image85 + + const handleRemovePhoto = useCallback( + item => { + onSelectPhotos(selectedPhotos.filter(filterItem => filterItem !== item)) + }, + [selectedPhotos, onSelectPhotos], + ) + + return selectedPhotos.length !== 0 ? ( + <View testID="selectedPhotosView" style={styles.gallery}> + {selectedPhotos.length !== 0 && + selectedPhotos.map((item, index) => ( + <View + key={`selected-image-${index}`} + style={[styles.imageContainer, imageStyle]}> + <TouchableOpacity + testID="removePhotoButton" + onPress={() => handleRemovePhoto(item)} + style={styles.removePhotoButton}> + <FontAwesomeIcon + icon="xmark" + size={16} + style={{color: colors.white}} + /> + </TouchableOpacity> + + <Image + testID="selectedPhotoImage" + style={[styles.image, imageStyle]} + source={{uri: item}} + /> + </View> + ))} + </View> + ) : null +} + +const styles = StyleSheet.create({ + gallery: { + flex: 1, + flexDirection: 'row', + marginTop: 16, + }, + imageContainer: { + margin: 2, + }, + image: { + resizeMode: 'cover', + borderRadius: 8, + }, + image250: { + width: 250, + height: 250, + }, + image175: { + width: 175, + height: 175, + }, + image85: { + width: 85, + height: 85, + }, + removePhotoButton: { + position: 'absolute', + top: 8, + right: 8, + width: 24, + height: 24, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.black, + zIndex: 1, + borderColor: colors.gray4, + borderWidth: 0.5, + }, +}) |