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,
KeyboardAvoidingView,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
@ -32,6 +33,7 @@ import {SelectedPhoto} from './SelectedPhoto'
const MAX_TEXT_LENGTH = 256
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
export const ComposePost = observer(function ComposePost({
replyTo,
@ -48,6 +50,7 @@ export const ComposePost = observer(function ComposePost({
const [processingState, setProcessingState] = useState('')
const [error, setError] = useState('')
const [text, setText] = useState('')
const [isSelectingPhotos, setIsSelectingPhotos] = useState(false)
const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
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[]) => {
setSelectedPhotos(photos)
setIsSelectingPhotos(false)
}
const onChangeText = (newText: string) => {
setText(newText)
@ -211,55 +221,71 @@ export const ComposePost = observer(function ComposePost({
<Text style={[s.red4, s.flex1]}>{error}</Text>
</View>
)}
{replyTo ? (
<View style={styles.replyToLayout}>
<ScrollView style={s.flex1}>
{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
handle={replyTo.author.handle}
displayName={replyTo.author.displayName}
avatar={replyTo.author.avatar}
handle={store.me.handle || ''}
displayName={store.me.displayName}
avatar={store.me.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>
<TextInput
ref={textInput}
multiline
scrollEnabled
onChangeText={(text: string) => onChangeText(text)}
placeholder={selectTextInputPlaceholder}
style={styles.textInput}>
{textDecorated}
</TextInput>
</View>
) : undefined}
<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
<SelectedPhoto
selectedPhotos={selectedPhotos}
onSelectPhotos={onSelectPhotos}
localPhotos={localPhotos}
/>
)}
</ScrollView>
{isSelectingPhotos &&
localPhotos.photos != null &&
selectedPhotos.length < 4 && (
<PhotoCarouselPicker
selectedPhotos={selectedPhotos}
onSelectPhotos={onSelectPhotos}
localPhotos={localPhotos}
/>
)}
<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} />
<Text style={[s.mr10, {color: progressColor}]}>
{MAX_TEXT_LENGTH - text.length}
@ -392,5 +418,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
borderTopWidth: 1,
borderTopColor: colors.gray2,
backgroundColor: colors.white,
},
})

View File

@ -86,6 +86,11 @@ export const PhotoCarouselPicker = ({
style={{color: colors.blue3}}
/>
</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) => (
<TouchableOpacity
key={`local-image-${index}`}
@ -94,11 +99,6 @@ export const PhotoCarouselPicker = ({
<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>
)
}
@ -109,6 +109,7 @@ const styles = StyleSheet.create({
maxHeight: 96,
padding: 8,
overflow: 'hidden',
backgroundColor: colors.white,
},
galleryButton: {
borderWidth: 1,

View File

@ -193,7 +193,7 @@ export const FeedItem = observer(function FeedItem({
style={styles.postText}
/>
</View>
<PostEmbeds embed={item.embed} style={{marginBottom: 10}} />
<PostEmbeds embed={item.embed} style={styles.postEmbeds} />
<PostCtrls
replyCount={item.replyCount}
repostCount={item.repostCount}
@ -278,4 +278,7 @@ const styles = StyleSheet.create({
fontSize: 16,
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 {
Image,
ImageStyle,
@ -8,6 +8,7 @@ import {
Text,
TouchableWithoutFeedback,
View,
ViewStyle,
} from 'react-native'
import {colors} from '../../../lib/styles'
@ -30,39 +31,29 @@ export function AutoSizedImage({
const [error, setError] = useState<string | undefined>()
const [imgInfo, setImgInfo] = 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(() => {
let aborted = false
Image.getSize(
uri,
(width: number, height: number) => {
if (!aborted) {
setImgInfo({width, height})
}
},
(error: any) => {
if (!aborted) {
setError(String(error))
}
},
)
if (!imgInfo) {
Image.getSize(
uri,
(width: number, height: number) => {
console.log('gotSize')
if (!aborted) {
setImgInfo({width, height})
}
},
(error: any) => {
if (!aborted) {
setError(String(error))
}
},
)
}
return () => {
aborted = true
}
}, [uri])
}, [uri, imgInfo])
const onLayout = (evt: LayoutChangeEvent) => {
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 (
<View style={style}>
<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 {faBookmark} from '@fortawesome/free-solid-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 {faCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck'
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 as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
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 {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
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 {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() {
@ -82,6 +83,7 @@ export function setup() {
farBell,
faBookmark,
farBookmark,
faCamera,
faCheck,
faCircleCheck,
faCircleUser,
@ -97,6 +99,8 @@ export function setup() {
faHeart,
fasHeart,
faHouse,
faImage,
farImage,
faLink,
faLock,
faMagnifyingGlass,
@ -122,8 +126,6 @@ export function setup() {
faTicket,
faTrashCan,
faX,
faCamera,
faImage,
faXmark,
)
}