Fixes to the composer UX around images and scrolling

zio/stable
Paul Frazee 2022-12-16 14:48:37 -06:00
parent 3aded6887d
commit 4ef3afb604
5 changed files with 116 additions and 80 deletions

View File

@ -4,6 +4,7 @@ import {
ActivityIndicator, ActivityIndicator,
KeyboardAvoidingView, KeyboardAvoidingView,
SafeAreaView, SafeAreaView,
ScrollView,
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
@ -32,6 +33,7 @@ import {SelectedPhoto} from './SelectedPhoto'
const MAX_TEXT_LENGTH = 256 const MAX_TEXT_LENGTH = 256
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
export const ComposePost = observer(function ComposePost({ export const ComposePost = observer(function ComposePost({
replyTo, replyTo,
@ -48,6 +50,7 @@ export const ComposePost = observer(function ComposePost({
const [processingState, setProcessingState] = useState('') const [processingState, setProcessingState] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [text, setText] = useState('') const [text, setText] = useState('')
const [isSelectingPhotos, setIsSelectingPhotos] = useState(false)
const [selectedPhotos, setSelectedPhotos] = useState<string[]>([]) const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
const autocompleteView = useMemo<UserAutocompleteViewModel>( const autocompleteView = useMemo<UserAutocompleteViewModel>(
@ -81,10 +84,17 @@ export const ComposePost = observer(function ComposePost({
} }
}, []) }, [])
const onPressSelectPhotos = () => {
if (isSelectingPhotos) {
setIsSelectingPhotos(false)
} else if (selectedPhotos.length < 4) {
setIsSelectingPhotos(true)
}
}
const onSelectPhotos = (photos: string[]) => { const onSelectPhotos = (photos: string[]) => {
setSelectedPhotos(photos) setSelectedPhotos(photos)
setIsSelectingPhotos(false)
} }
const onChangeText = (newText: string) => { const onChangeText = (newText: string) => {
setText(newText) setText(newText)
@ -211,55 +221,71 @@ export const ComposePost = observer(function ComposePost({
<Text style={[s.red4, s.flex1]}>{error}</Text> <Text style={[s.red4, s.flex1]}>{error}</Text>
</View> </View>
)} )}
{replyTo ? ( <ScrollView style={s.flex1}>
<View style={styles.replyToLayout}> {replyTo ? (
<View style={styles.replyToLayout}>
<UserAvatar
handle={replyTo.author.handle}
displayName={replyTo.author.displayName}
avatar={replyTo.author.avatar}
size={50}
/>
<View style={styles.replyToPost}>
<TextLink
href={`/profile/${replyTo.author.handle}`}
text={replyTo.author.displayName || replyTo.author.handle}
style={[s.f16, s.bold]}
/>
<Text style={[s.f16, s['lh16-1.3']]} numberOfLines={6}>
{replyTo.text}
</Text>
</View>
</View>
) : undefined}
<View style={[styles.textInputLayout, selectTextInputLayout]}>
<UserAvatar <UserAvatar
handle={replyTo.author.handle} handle={store.me.handle || ''}
displayName={replyTo.author.displayName} displayName={store.me.displayName}
avatar={replyTo.author.avatar} avatar={store.me.avatar}
size={50} size={50}
/> />
<View style={styles.replyToPost}> <TextInput
<TextLink ref={textInput}
href={`/profile/${replyTo.author.handle}`} multiline
text={replyTo.author.displayName || replyTo.author.handle} scrollEnabled
style={[s.f16, s.bold]} onChangeText={(text: string) => onChangeText(text)}
/> placeholder={selectTextInputPlaceholder}
<Text style={[s.f16, s['lh16-1.3']]} numberOfLines={6}> style={styles.textInput}>
{replyTo.text} {textDecorated}
</Text> </TextInput>
</View>
</View> </View>
) : undefined} <SelectedPhoto
<View style={[styles.textInputLayout, selectTextInputLayout]}>
<UserAvatar
handle={store.me.handle || ''}
displayName={store.me.displayName}
avatar={store.me.avatar}
size={50}
/>
<TextInput
ref={textInput}
multiline
scrollEnabled
onChangeText={(text: string) => onChangeText(text)}
placeholder={selectTextInputPlaceholder}
style={styles.textInput}>
{textDecorated}
</TextInput>
</View>
<SelectedPhoto
selectedPhotos={selectedPhotos}
onSelectPhotos={onSelectPhotos}
/>
{localPhotos.photos != null && selectedPhotos.length < 4 && (
<PhotoCarouselPicker
selectedPhotos={selectedPhotos} selectedPhotos={selectedPhotos}
onSelectPhotos={onSelectPhotos} onSelectPhotos={onSelectPhotos}
localPhotos={localPhotos}
/> />
)} </ScrollView>
{isSelectingPhotos &&
localPhotos.photos != null &&
selectedPhotos.length < 4 && (
<PhotoCarouselPicker
selectedPhotos={selectedPhotos}
onSelectPhotos={onSelectPhotos}
localPhotos={localPhotos}
/>
)}
<View style={styles.bottomBar}> <View style={styles.bottomBar}>
<TouchableOpacity
onPress={onPressSelectPhotos}
style={[s.pl5]}
hitSlop={HITSLOP}>
<FontAwesomeIcon
icon={['far', 'image']}
style={{
color: selectedPhotos.length < 4 ? colors.blue3 : colors.gray3,
}}
size={24}
/>
</TouchableOpacity>
<View style={s.flex1} /> <View style={s.flex1} />
<Text style={[s.mr10, {color: progressColor}]}> <Text style={[s.mr10, {color: progressColor}]}>
{MAX_TEXT_LENGTH - text.length} {MAX_TEXT_LENGTH - text.length}
@ -392,5 +418,6 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: colors.gray2, borderTopColor: colors.gray2,
backgroundColor: colors.white,
}, },
}) })

View File

@ -86,6 +86,11 @@ export const PhotoCarouselPicker = ({
style={{color: colors.blue3}} style={{color: colors.blue3}}
/> />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity
style={[styles.galleryButton, styles.photo]}
onPress={handleOpenGallery}>
<FontAwesomeIcon icon="image" style={{color: colors.blue3}} size={24} />
</TouchableOpacity>
{localPhotos.photos.map((item: any, index: number) => ( {localPhotos.photos.map((item: any, index: number) => (
<TouchableOpacity <TouchableOpacity
key={`local-image-${index}`} key={`local-image-${index}`}
@ -94,11 +99,6 @@ export const PhotoCarouselPicker = ({
<Image style={styles.photo} source={{uri: item.node.image.uri}} /> <Image style={styles.photo} source={{uri: item.node.image.uri}} />
</TouchableOpacity> </TouchableOpacity>
))} ))}
<TouchableOpacity
style={[styles.galleryButton, styles.photo]}
onPress={handleOpenGallery}>
<FontAwesomeIcon icon="image" style={{color: colors.blue3}} size={24} />
</TouchableOpacity>
</ScrollView> </ScrollView>
) )
} }
@ -109,6 +109,7 @@ const styles = StyleSheet.create({
maxHeight: 96, maxHeight: 96,
padding: 8, padding: 8,
overflow: 'hidden', overflow: 'hidden',
backgroundColor: colors.white,
}, },
galleryButton: { galleryButton: {
borderWidth: 1, borderWidth: 1,

View File

@ -193,7 +193,7 @@ export const FeedItem = observer(function FeedItem({
style={styles.postText} style={styles.postText}
/> />
</View> </View>
<PostEmbeds embed={item.embed} style={{marginBottom: 10}} /> <PostEmbeds embed={item.embed} style={styles.postEmbeds} />
<PostCtrls <PostCtrls
replyCount={item.replyCount} replyCount={item.replyCount}
repostCount={item.repostCount} repostCount={item.repostCount}
@ -278,4 +278,7 @@ const styles = StyleSheet.create({
fontSize: 16, fontSize: 16,
lineHeight: 20.8, // 1.3 of 16px lineHeight: 20.8, // 1.3 of 16px
}, },
postEmbeds: {
marginBottom: 10,
},
}) })

View File

@ -1,4 +1,4 @@
import React, {useState, useEffect, useMemo} from 'react' import React, {useState, useEffect} from 'react'
import { import {
Image, Image,
ImageStyle, ImageStyle,
@ -8,6 +8,7 @@ import {
Text, Text,
TouchableWithoutFeedback, TouchableWithoutFeedback,
View, View,
ViewStyle,
} from 'react-native' } from 'react-native'
import {colors} from '../../../lib/styles' import {colors} from '../../../lib/styles'
@ -30,39 +31,29 @@ export function AutoSizedImage({
const [error, setError] = useState<string | undefined>() const [error, setError] = useState<string | undefined>()
const [imgInfo, setImgInfo] = useState<Dim | undefined>() const [imgInfo, setImgInfo] = useState<Dim | undefined>()
const [containerInfo, setContainerInfo] = useState<Dim | undefined>() const [containerInfo, setContainerInfo] = useState<Dim | undefined>()
const calculatedStyle = useMemo(() => {
if (imgInfo && containerInfo) {
// imgInfo.height / imgInfo.width = x / containerInfo.width
// x = imgInfo.height / imgInfo.width * containerInfo.width
return {
height: Math.min(
MAX_HEIGHT,
(imgInfo.height / imgInfo.width) * containerInfo.width,
),
}
}
return undefined
}, [imgInfo, containerInfo])
useEffect(() => { useEffect(() => {
let aborted = false let aborted = false
Image.getSize( if (!imgInfo) {
uri, Image.getSize(
(width: number, height: number) => { uri,
if (!aborted) { (width: number, height: number) => {
setImgInfo({width, height}) console.log('gotSize')
} if (!aborted) {
}, setImgInfo({width, height})
(error: any) => { }
if (!aborted) { },
setError(String(error)) (error: any) => {
} if (!aborted) {
}, setError(String(error))
) }
},
)
}
return () => { return () => {
aborted = true aborted = true
} }
}, [uri]) }, [uri, imgInfo])
const onLayout = (evt: LayoutChangeEvent) => { const onLayout = (evt: LayoutChangeEvent) => {
setContainerInfo({ setContainerInfo({
@ -71,6 +62,18 @@ export function AutoSizedImage({
}) })
} }
let calculatedStyle: StyleProp<ViewStyle> | undefined
if (imgInfo && containerInfo) {
// imgInfo.height / imgInfo.width = x / containerInfo.width
// x = imgInfo.height / imgInfo.width * containerInfo.width
calculatedStyle = {
height: Math.min(
MAX_HEIGHT,
(imgInfo.height / imgInfo.width) * containerInfo.width,
),
}
}
return ( return (
<View style={style}> <View style={style}>
<TouchableWithoutFeedback onPress={onPress}> <TouchableWithoutFeedback onPress={onPress}>

View File

@ -18,6 +18,7 @@ import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell'
import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark' import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark'
import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark' import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark'
import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera'
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck' import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'
import {faCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck' import {faCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck'
import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser' import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser'
@ -33,6 +34,8 @@ import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe'
import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart'
import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart' import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse' import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
import {faImage as farImage} from '@fortawesome/free-regular-svg-icons/faImage'
import {faImage} from '@fortawesome/free-solid-svg-icons/faImage'
import {faLink} from '@fortawesome/free-solid-svg-icons/faLink' import {faLink} from '@fortawesome/free-solid-svg-icons/faLink'
import {faLock} from '@fortawesome/free-solid-svg-icons/faLock' import {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
@ -58,8 +61,6 @@ import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark'
import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
import {faX} from '@fortawesome/free-solid-svg-icons/faX' 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' import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
export function setup() { export function setup() {
@ -82,6 +83,7 @@ export function setup() {
farBell, farBell,
faBookmark, faBookmark,
farBookmark, farBookmark,
faCamera,
faCheck, faCheck,
faCircleCheck, faCircleCheck,
faCircleUser, faCircleUser,
@ -97,6 +99,8 @@ export function setup() {
faHeart, faHeart,
fasHeart, fasHeart,
faHouse, faHouse,
faImage,
farImage,
faLink, faLink,
faLock, faLock,
faMagnifyingGlass, faMagnifyingGlass,
@ -122,8 +126,6 @@ export function setup() {
faTicket, faTicket,
faTrashCan, faTrashCan,
faX, faX,
faCamera,
faImage,
faXmark, faXmark,
) )
} }