diff options
author | João Ferreiro <joaoferreiro5@gmail.com> | 2022-12-02 16:41:01 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-02 10:41:01 -0600 |
commit | 67c4dcff3731444c6e6eadcbed1d55bd503bda4a (patch) | |
tree | fc15ad58ca5694135851e189faf8a389ff5c933d /src | |
parent | 7ae1bac6208c6c00363aae344c76261a28433614 (diff) | |
download | voidsky-67c4dcff3731444c6e6eadcbed1d55bd503bda4a.tar.zst |
Upload image in composer (#27)
* upload images in composer v1 * fix android compile * reafctor image carousel into new component; fix photo overlapping text in composer * revert android changes * further refactoring code into different components * move show carousel out of the component * fixing add photo using camera * fix typescript issue; force mediatype photo * change post test with photo attached; remove auto linking settings * use runInAction in getPhotos model * react-hooks/exhaustive-deps fixes * crop every photo; make use of useCallback * moving placeholder condition * Cleanup Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/user-local-photos.ts | 27 | ||||
-rw-r--r-- | src/view/com/composer/ComposePost.tsx | 65 | ||||
-rw-r--r-- | src/view/com/composer/PhotoCarouselPicker.tsx | 128 | ||||
-rw-r--r-- | src/view/com/composer/SelectedPhoto.tsx | 87 | ||||
-rw-r--r-- | src/view/index.ts | 6 |
5 files changed, 304 insertions, 9 deletions
diff --git a/src/state/models/user-local-photos.ts b/src/state/models/user-local-photos.ts new file mode 100644 index 000000000..9a1455039 --- /dev/null +++ b/src/state/models/user-local-photos.ts @@ -0,0 +1,27 @@ +import {PhotoIdentifier} from './../../../node_modules/@react-native-camera-roll/camera-roll/src/CameraRoll' +import {makeAutoObservable, runInAction} from 'mobx' +import {CameraRoll} from '@react-native-camera-roll/camera-roll' +import {RootStoreModel} from './root-store' + +export class UserLocalPhotosModel { + // state + photos: PhotoIdentifier[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable(this, { + rootStore: false, + }) + } + + async setup() { + await this._getPhotos() + } + + private async _getPhotos() { + CameraRoll.getPhotos({first: 20}).then(r => { + runInAction(() => { + this.photos = r.edges + }) + }) + } +} diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index a61759c24..b43f4ab9e 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -23,6 +23,9 @@ import * as apilib from '../../../state/lib/api' import {ComposerOpts} from '../../../state/models/shell-ui' import {s, colors, gradients} from '../../lib/styles' import {detectLinkables} from '../../../lib/strings' +import {UserLocalPhotosModel} from '../../../state/models/user-local-photos' +import {PhotoCarouselPicker} from './PhotoCarouselPicker' +import {SelectedPhoto} from './SelectedPhoto' const MAX_TEXT_LENGTH = 256 const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH @@ -41,14 +44,22 @@ export const ComposePost = observer(function ComposePost({ const [isProcessing, setIsProcessing] = useState(false) const [error, setError] = useState('') const [text, setText] = useState('') + const [selectedPhotos, setSelectedPhotos] = useState<string[]>([]) + const autocompleteView = useMemo<UserAutocompleteViewModel>( () => new UserAutocompleteViewModel(store), - [], + [store], + ) + const localPhotos = useMemo<UserLocalPhotosModel>( + () => new UserLocalPhotosModel(store), + [store], ) useEffect(() => { autocompleteView.setup() - }) + localPhotos.setup() + }, [autocompleteView, localPhotos]) + useEffect(() => { // HACK // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view @@ -60,9 +71,11 @@ export const ComposePost = observer(function ComposePost({ }, 250) } return () => { - if (to) clearTimeout(to) + if (to) { + clearTimeout(to) + } } - }, [textInput.current]) + }, []) const onChangeText = (newText: string) => { setText(newText) @@ -116,6 +129,16 @@ export const ComposePost = observer(function ComposePost({ const canPost = text.length <= MAX_TEXT_LENGTH const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined + const selectTextInputLayout = + selectedPhotos.length !== 0 + ? styles.textInputLayoutWithPhoto + : styles.textInputLayoutWithoutPhoto + const selectTextInputPlaceholder = replyTo + ? 'Write your reply' + : selectedPhotos.length !== 0 + ? 'Write a comment' + : "What's up?" + const textDecorated = useMemo(() => { let i = 0 return detectLinkables(text).map(v => { @@ -192,7 +215,7 @@ export const ComposePost = observer(function ComposePost({ </View> </View> ) : undefined} - <View style={styles.textInputLayout}> + <View style={[styles.textInputLayout, selectTextInputLayout]}> <UserAvatar handle={store.me.handle || ''} displayName={store.me.displayName} @@ -203,13 +226,26 @@ export const ComposePost = observer(function ComposePost({ multiline scrollEnabled onChangeText={(text: string) => onChangeText(text)} - placeholder={replyTo ? 'Write your reply' : "What's up?"} + placeholder={selectTextInputPlaceholder} style={styles.textInput}> {textDecorated} </TextInput> </View> - <View - style={[s.flexRow, {alignItems: 'center'}, s.pt10, s.pb10, s.pr5]}> + <SelectedPhoto + selectedPhotos={selectedPhotos} + setSelectedPhotos={setSelectedPhotos} + /> + {localPhotos.photos != null && + text === '' && + selectedPhotos.length === 0 && ( + <PhotoCarouselPicker + selectedPhotos={selectedPhotos} + setSelectedPhotos={setSelectedPhotos} + localPhotos={localPhotos} + /> + )} + <View style={styles.separator} /> + <View style={[s.flexRow, s.pt10, s.pb10, s.pr5, styles.contentCenter]}> <View style={s.flex1} /> <Text style={[s.mr10, {color: progressColor}]}> {MAX_TEXT_LENGTH - text.length} @@ -282,9 +318,14 @@ const styles = StyleSheet.create({ justifyContent: 'center', marginRight: 5, }, + textInputLayoutWithPhoto: { + flexWrap: 'wrap', + }, + textInputLayoutWithoutPhoto: { + flex: 1, + }, textInputLayout: { flexDirection: 'row', - flex: 1, borderTopWidth: 1, borderTopColor: colors.gray2, paddingTop: 16, @@ -307,4 +348,10 @@ const styles = StyleSheet.create({ paddingLeft: 13, paddingRight: 8, }, + contentCenter: {alignItems: 'center'}, + separator: { + borderBottomColor: 'black', + borderBottomWidth: StyleSheet.hairlineWidth, + width: '100%', + }, }) diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/PhotoCarouselPicker.tsx new file mode 100644 index 000000000..f4af4c61e --- /dev/null +++ b/src/view/com/composer/PhotoCarouselPicker.tsx @@ -0,0 +1,128 @@ +import React, {useCallback} from 'react' +import {Image, StyleSheet, TouchableOpacity, ScrollView} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {colors} from '../../lib/styles' +import { + openPicker, + openCamera, + openCropper, +} from 'react-native-image-crop-picker' + +export const PhotoCarouselPicker = ({ + selectedPhotos, + setSelectedPhotos, + localPhotos, +}: { + selectedPhotos: string[] + setSelectedPhotos: React.Dispatch<React.SetStateAction<string[]>> + localPhotos: any +}) => { + const handleOpenCamera = useCallback(() => { + openCamera({ + mediaType: 'photo', + cropping: true, + width: 1000, + height: 1000, + }).then( + item => { + setSelectedPhotos([item.path, ...selectedPhotos]) + }, + _err => { + // ignore + }, + ) + }, [selectedPhotos, setSelectedPhotos]) + + const handleSelectPhoto = useCallback( + async (uri: string) => { + const img = await openCropper({ + mediaType: 'photo', + path: uri, + width: 1000, + height: 1000, + }) + setSelectedPhotos([img.path, ...selectedPhotos]) + }, + [selectedPhotos, setSelectedPhotos], + ) + + const handleOpenGallery = useCallback(() => { + openPicker({ + multiple: true, + maxFiles: 4, + mediaType: 'photo', + }).then(async items => { + const result = [] + + for await (const image of items) { + const img = await openCropper({ + mediaType: 'photo', + path: image.path, + width: 1000, + height: 1000, + }) + result.push(img.path) + } + setSelectedPhotos([...result, ...selectedPhotos]) + }) + }, [selectedPhotos, setSelectedPhotos]) + + return ( + <ScrollView + horizontal + style={styles.photosContainer} + showsHorizontalScrollIndicator={false}> + <TouchableOpacity + style={[styles.galleryButton, styles.photo]} + onPress={handleOpenCamera}> + <FontAwesomeIcon + icon="camera" + size={24} + style={{color: colors.blue3}} + /> + </TouchableOpacity> + {localPhotos.photos.map((item: any, index: number) => ( + <TouchableOpacity + key={`local-image-${index}`} + style={styles.photoButton} + onPress={() => handleSelectPhoto(item.node.image.uri)}> + <Image style={styles.photo} source={{uri: item.node.image.uri}} /> + </TouchableOpacity> + ))} + <TouchableOpacity + style={[styles.galleryButton, styles.photo]} + onPress={handleOpenGallery}> + <FontAwesomeIcon icon="image" style={{color: colors.blue3}} size={24} /> + </TouchableOpacity> + </ScrollView> + ) +} + +const styles = StyleSheet.create({ + photosContainer: { + width: '100%', + maxHeight: 96, + padding: 8, + overflow: 'hidden', + }, + galleryButton: { + borderWidth: 1, + borderColor: colors.gray3, + alignItems: 'center', + justifyContent: 'center', + }, + photoButton: { + width: 75, + height: 75, + marginRight: 8, + borderWidth: 1, + borderRadius: 16, + borderColor: colors.gray3, + }, + photo: { + width: 75, + height: 75, + marginRight: 8, + borderRadius: 16, + }, +}) diff --git a/src/view/com/composer/SelectedPhoto.tsx b/src/view/com/composer/SelectedPhoto.tsx new file mode 100644 index 000000000..88209b3df --- /dev/null +++ b/src/view/com/composer/SelectedPhoto.tsx @@ -0,0 +1,87 @@ +import React, {useCallback} from 'react' +import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {colors} from '../../lib/styles' + +export const SelectedPhoto = ({ + selectedPhotos, + setSelectedPhotos, +}: { + selectedPhotos: string[] + setSelectedPhotos: React.Dispatch<React.SetStateAction<string[]>> +}) => { + const imageStyle = + selectedPhotos.length === 1 + ? styles.image250 + : selectedPhotos.length === 2 + ? styles.image175 + : styles.image85 + + const handleRemovePhoto = useCallback( + item => { + setSelectedPhotos( + selectedPhotos.filter(filterItem => filterItem !== item), + ) + }, + [selectedPhotos, setSelectedPhotos], + ) + + return selectedPhotos.length !== 0 ? ( + <View style={styles.imageContainer}> + {selectedPhotos.length !== 0 && + selectedPhotos.map((item, index) => ( + <View + key={`selected-image-${index}`} + style={[styles.image, imageStyle]}> + <TouchableOpacity + onPress={() => handleRemovePhoto(item)} + style={styles.removePhotoButton}> + <FontAwesomeIcon + icon="xmark" + size={16} + style={{color: colors.white}} + /> + </TouchableOpacity> + + <Image style={[styles.image, imageStyle]} source={{uri: item}} /> + </View> + ))} + </View> + ) : null +} + +const styles = StyleSheet.create({ + imageContainer: { + flex: 1, + flexDirection: 'row', + marginTop: 16, + }, + image: { + borderRadius: 8, + margin: 2, + }, + 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, + }, +}) diff --git a/src/view/index.ts b/src/view/index.ts index e38e1debf..bd0e33cbe 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -56,6 +56,9 @@ import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark' import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faX} from '@fortawesome/free-solid-svg-icons/faX' +import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera' +import {faImage} from '@fortawesome/free-solid-svg-icons/faImage' +import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' export function setup() { library.add( @@ -115,5 +118,8 @@ export function setup() { faTicket, faTrashCan, faX, + faCamera, + faImage, + faXmark, ) } |