Fixes to the composer UX around images and scrolling
parent
3aded6887d
commit
4ef3afb604
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue