Add the ability to paste images into the composer (#56)

This commit is contained in:
Paul Frazee 2023-01-19 17:36:49 -06:00 committed by GitHub
parent 15589d216f
commit 570b76a71d
5 changed files with 101 additions and 34 deletions

View file

@ -7,11 +7,14 @@ import {
SafeAreaView,
ScrollView,
StyleSheet,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native'
import PasteInput, {
PastedFile,
PasteInputRef,
} from '@mattermost/react-native-paste-input'
import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view'
@ -33,7 +36,7 @@ import {detectLinkables, extractEntities} from '../../../lib/strings'
import {getLinkMeta} from '../../../lib/link-meta'
import {downloadAndResize} from '../../../lib/images'
import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
import {PhotoCarouselPicker} from './PhotoCarouselPicker'
import {PhotoCarouselPicker, cropPhoto} from './PhotoCarouselPicker'
import {SelectedPhoto} from './SelectedPhoto'
import {usePalette} from '../../lib/hooks/usePalette'
@ -54,7 +57,7 @@ export const ComposePost = observer(function ComposePost({
}) {
const pal = usePalette('default')
const store = useStores()
const textInput = useRef<TextInput>(null)
const textInput = useRef<PasteInputRef>(null)
const [isProcessing, setIsProcessing] = useState(false)
const [processingState, setProcessingState] = useState('')
const [error, setError] = useState('')
@ -78,6 +81,16 @@ export const ComposePost = observer(function ComposePost({
[store],
)
// HACK
// there's a bug with @mattermost/react-native-paste-input where if the input
// is focused during unmount, an exception will throw (seems that a blur method isnt implemented)
// manually blurring before closing gets around that
// -prf
const hackfixOnClose = () => {
textInput.current?.blur()
onClose()
}
// initial setup
useEffect(() => {
autocompleteView.setup()
@ -134,7 +147,8 @@ export const ComposePost = observer(function ComposePost({
isLoading: false, // done
})
}
}, [extLink])
return cleanup
}, [store, extLink])
useEffect(() => {
// HACK
@ -196,9 +210,21 @@ export const ComposePost = observer(function ComposePost({
}
}
}
const onPressCancel = () => {
onClose()
const onPaste = async (err: string | undefined, files: PastedFile[]) => {
if (err) {
return setError(err)
}
if (selectedPhotos.length >= 4) {
return
}
const imgFile = files.find(file => /\.(jpe?g|png)$/.test(file.fileName))
if (!imgFile) {
return
}
const finalImgPath = await cropPhoto(imgFile.uri)
onSelectPhotos([...selectedPhotos, finalImgPath])
}
const onPressCancel = () => hackfixOnClose()
const onPressPublish = async () => {
if (isProcessing) {
return
@ -229,7 +255,7 @@ export const ComposePost = observer(function ComposePost({
}
store.me.mainFeed.loadLatest()
onPost?.()
onClose()
hackfixOnClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
}
const onSelectAutocompleteItem = (item: string) => {
@ -254,7 +280,11 @@ export const ComposePost = observer(function ComposePost({
let i = 0
return detectLinkables(text).map(v => {
if (typeof v === 'string') {
return v
return (
<Text key={i++} style={styles.textInputFormatting}>
{v}
</Text>
)
} else {
return (
<Text key={i++} style={[pal.link, styles.textInputFormatting]}>
@ -263,7 +293,7 @@ export const ComposePost = observer(function ComposePost({
)
}
})
}, [text])
}, [text, pal.link])
return (
<KeyboardAvoidingView
@ -354,12 +384,13 @@ export const ComposePost = observer(function ComposePost({
avatar={store.me.avatar}
size={50}
/>
<TextInput
<PasteInput
testID="composerTextInput"
ref={textInput}
multiline
scrollEnabled
onChangeText={(text: string) => onChangeText(text)}
onPaste={onPaste}
placeholder={selectTextInputPlaceholder}
placeholderTextColor={pal.colors.textLight}
style={[
@ -368,7 +399,7 @@ export const ComposePost = observer(function ComposePost({
styles.textInputFormatting,
]}>
{textDecorated}
</TextInput>
</PasteInput>
</View>
<SelectedPhoto
selectedPhotos={selectedPhotos}

View file

@ -26,6 +26,28 @@ const IMAGE_PARAMS = {
compressImageQuality: 1.0,
}
export async function cropPhoto(
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({
mediaType: 'photo',
path,
...IMAGE_PARAMS,
width,
height,
})
const img = await compressIfNeeded(cropperRes, MAX_SIZE)
return img.path
}
export const PhotoCarouselPicker = ({
selectedPhotos,
onSelectPhotos,
@ -55,21 +77,12 @@ export const PhotoCarouselPicker = ({
const handleSelectPhoto = useCallback(
async (item: PhotoIdentifier) => {
try {
// choose target dimensions based on the original
// this causes the photo cropper to start with the full image "selected"
const {width, height} = scaleDownDimensions(
{width: item.node.image.width, height: item.node.image.height},
{width: MAX_WIDTH, height: MAX_HEIGHT},
const imgPath = await cropPhoto(
item.node.image.uri,
item.node.image.width,
item.node.image.height,
)
const cropperRes = await openCropper({
mediaType: 'photo',
path: item.node.image.uri,
...IMAGE_PARAMS,
width,
height,
})
const img = await compressIfNeeded(cropperRes, MAX_SIZE)
onSelectPhotos([...selectedPhotos, img.path])
onSelectPhotos([...selectedPhotos, imgPath])
} catch (err: any) {
// ignore
store.log.warn('Error selecting photo', err)