add expandable context to composer when replying to post (#2419)

* add expand replyTo text with animation

* add images, quote to replyTo

* support withmedia

* adjust layout

* add embed to all needed openComposer calls

* adjust gap

* organize imports
zio/stable
Hailey 2024-01-08 21:37:12 -08:00 committed by GitHub
parent 153c25e1fe
commit dda5ca27fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 261 additions and 29 deletions

View File

@ -11,6 +11,7 @@ export interface ComposerOptsPostRef {
displayName?: string displayName?: string
avatar?: string avatar?: string
} }
embed?: AppBskyEmbedRecord.ViewRecord['embed']
} }
export interface ComposerOptsQuote { export interface ComposerOptsQuote {
uri: string uri: string

View File

@ -29,8 +29,6 @@ import {UserAvatar} from '../util/UserAvatar'
import * as apilib from 'lib/api/index' import * as apilib from 'lib/api/index'
import {ComposerOpts} from 'state/shell/composer' import {ComposerOpts} from 'state/shell/composer'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {shortenLinks} from 'lib/strings/rich-text-manip' import {shortenLinks} from 'lib/strings/rich-text-manip'
import {toShortUrl} from 'lib/strings/url-helpers' import {toShortUrl} from 'lib/strings/url-helpers'
@ -63,6 +61,7 @@ import {useComposerControls} from '#/state/shell/composer'
import {emitPostCreated} from '#/state/events' import {emitPostCreated} from '#/state/events'
import {ThreadgateSetting} from '#/state/queries/threadgate' import {ThreadgateSetting} from '#/state/queries/threadgate'
import {logger} from '#/logger' import {logger} from '#/logger'
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
type Props = ComposerOpts type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({ export const ComposePost = observer(function ComposePost({
@ -379,22 +378,7 @@ export const ComposePost = observer(function ComposePost({
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
keyboardShouldPersistTaps="always"> keyboardShouldPersistTaps="always">
{replyTo ? ( {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<View style={[pal.border, styles.replyToLayout]}>
<UserAvatar avatar={replyTo.author.avatar} size={50} />
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>
{sanitizeDisplayName(
replyTo.author.displayName ||
sanitizeHandle(replyTo.author.handle),
)}
</Text>
<Text type="post-text" style={pal.text} numberOfLines={6}>
{replyTo.text}
</Text>
</View>
</View>
) : undefined}
<View <View
style={[ style={[
@ -549,17 +533,6 @@ const styles = StyleSheet.create({
textInputLayoutMobile: { textInputLayoutMobile: {
flex: 1, flex: 1,
}, },
replyToLayout: {
flexDirection: 'row',
borderTopWidth: 1,
paddingTop: 16,
paddingBottom: 16,
},
replyToPost: {
flex: 1,
paddingLeft: 13,
paddingRight: 8,
},
addExtLinkBtn: { addExtLinkBtn: {
borderWidth: 1, borderWidth: 1,
borderRadius: 24, borderRadius: 24,

View File

@ -0,0 +1,254 @@
import React from 'react'
import {LayoutAnimation, Pressable, StyleSheet, View} from 'react-native'
import {Image} from 'expo-image'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
} from '@atproto/api'
import {ComposerOptsPostRef} from 'state/shell/composer'
import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {Text} from 'view/com/util/text/Text'
import QuoteEmbed from 'view/com/util/post-embeds/QuoteEmbed'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const pal = usePalette('default')
const {_} = useLingui()
const {embed} = replyTo
const [showFull, setShowFull] = React.useState(false)
const onPress = React.useCallback(() => {
setShowFull(prev => !prev)
LayoutAnimation.configureNext({
duration: 350,
update: {type: 'spring', springDamping: 0.7},
})
}, [])
const quote = React.useMemo(() => {
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record) &&
AppBskyFeedPost.isRecord(embed.record.value)
) {
// Not going to include the images right now
return {
author: embed.record.author,
cid: embed.record.cid,
uri: embed.record.uri,
indexedAt: embed.record.indexedAt,
text: embed.record.value.text,
}
} else if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
AppBskyFeedPost.isRecord(embed.record.record.value)
) {
return {
author: embed.record.record.author,
cid: embed.record.record.cid,
uri: embed.record.record.uri,
indexedAt: embed.record.record.indexedAt,
text: embed.record.record.value.text,
}
}
}, [embed])
const images = React.useMemo(() => {
if (AppBskyEmbedImages.isView(embed)) {
return embed.images
} else if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedImages.isView(embed.media)
) {
return embed.media.images
}
}, [embed])
return (
<Pressable
style={[pal.border, styles.replyToLayout]}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Expand or collapse the full post you are replying to`,
)}
accessibilityHint={_(
msg`Expand or collapse the full post you are replying to`,
)}>
<UserAvatar avatar={replyTo.author.avatar} size={50} />
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>
{sanitizeDisplayName(
replyTo.author.displayName || sanitizeHandle(replyTo.author.handle),
)}
</Text>
<View style={styles.replyToBody}>
<View style={styles.replyToText}>
<Text
type="post-text"
style={pal.text}
numberOfLines={!showFull ? 6 : undefined}>
{replyTo.text}
</Text>
</View>
{images && (
<ComposerReplyToImages images={images} showFull={showFull} />
)}
</View>
{showFull && quote && <QuoteEmbed quote={quote} />}
</View>
</Pressable>
)
}
function ComposerReplyToImages({
images,
}: {
images: AppBskyEmbedImages.ViewImage[]
showFull: boolean
}) {
return (
<View
style={{
width: 65,
flexDirection: 'column',
alignItems: 'center',
}}>
<View style={styles.imagesContainer}>
{(images.length === 1 && (
<Image
source={{uri: images[0].thumb}}
style={styles.singleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
)) ||
(images.length === 2 && (
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImageTall}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImageTall}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
)) ||
(images.length === 3 && (
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImageTall}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<View style={styles.imagesInner}>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[2].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
)) ||
(images.length === 4 && (
<View style={styles.imagesInner}>
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[2].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[3].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
))}
</View>
</View>
)
}
const styles = StyleSheet.create({
replyToLayout: {
flexDirection: 'row',
borderTopWidth: 1,
paddingTop: 16,
paddingBottom: 16,
},
replyToPost: {
flex: 1,
paddingLeft: 13,
paddingRight: 8,
},
replyToBody: {
flexDirection: 'row',
gap: 10,
},
replyToText: {
flex: 1,
flexGrow: 1,
},
imagesContainer: {
borderRadius: 6,
overflow: 'hidden',
marginTop: 2,
},
imagesInner: {
gap: 2,
},
imagesRow: {
flexDirection: 'row',
},
singleImage: {
width: 65,
height: 65,
},
doubleImageTall: {
width: 32.5,
height: 65,
},
doubleImage: {
width: 32.5,
height: 32.5,
},
})

View File

@ -214,6 +214,7 @@ let PostThreadItemLoaded = ({
displayName: post.author.displayName, displayName: post.author.displayName,
avatar: post.author.avatar, avatar: post.author.avatar,
}, },
embed: post.embed,
}, },
onPost: onPostReply, onPost: onPostReply,
}) })

View File

@ -118,6 +118,7 @@ function PostInner({
displayName: post.author.displayName, displayName: post.author.displayName,
avatar: post.author.avatar, avatar: post.author.avatar,
}, },
embed: post.embed,
}, },
}) })
}, [openComposer, post, record]) }, [openComposer, post, record])

View File

@ -131,6 +131,7 @@ let FeedItemInner = ({
displayName: post.author.displayName, displayName: post.author.displayName,
avatar: post.author.avatar, avatar: post.author.avatar,
}, },
embed: post.embed,
}, },
}) })
}, [post, record, openComposer]) }, [post, record, openComposer])

View File

@ -67,6 +67,7 @@ export function PostThreadScreen({route}: Props) {
displayName: thread.post.author.displayName, displayName: thread.post.author.displayName,
avatar: thread.post.author.avatar, avatar: thread.post.author.avatar,
}, },
embed: thread.post.embed,
}, },
onPost: () => onPost: () =>
queryClient.invalidateQueries({ queryClient.invalidateQueries({