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