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 importszio/stable
parent
153c25e1fe
commit
dda5ca27fe
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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({
|
||||||
|
|
Loading…
Reference in New Issue