import {useCallback, useMemo, useState} from 'react' import {LayoutAnimation, Pressable, View} from 'react-native' import {Image} from 'expo-image' import { AppBskyEmbedGallery, AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyFeedPost, } from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {type ComposerOptsPostRef} from '#/state/shell/composer' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, utils, web} from '#/alf' import {QuoteEmbed} from '#/components/Post/Embed' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {parseEmbed} from '#/types/bsky/post' export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { const t = useTheme() const {_} = useLingui() const {embed} = replyTo const [showFull, setShowFull] = useState(false) const onPress = useCallback(() => { setShowFull(prev => !prev) LayoutAnimation.configureNext({ duration: 350, update: {type: 'spring', springDamping: 0.7}, }) }, []) const quoteEmbed = useMemo(() => { if ( AppBskyEmbedRecord.isView(embed) && AppBskyEmbedRecord.isViewRecord(embed.record) && AppBskyFeedPost.isRecord(embed.record.value) ) { return embed } else if ( AppBskyEmbedRecordWithMedia.isView(embed) && AppBskyEmbedRecord.isViewRecord(embed.record.record) && AppBskyFeedPost.isRecord(embed.record.record.value) ) { return embed.record } return null }, [embed]) const parsedQuoteEmbed = quoteEmbed ? parseEmbed({ $type: 'app.bsky.embed.record#view', ...quoteEmbed, }) : null const {images, totalNumber} = useMemo(() => { if (AppBskyEmbedImages.isView(embed)) { return {images: embed.images, totalNumber: embed.images.length} } else if (AppBskyEmbedGallery.isView(embed)) { return { images: galleryItemsToImages(embed.items), totalNumber: embed.items.length, } } else if (AppBskyEmbedRecordWithMedia.isView(embed)) { if (AppBskyEmbedImages.isView(embed.media)) { return { images: embed.media.images, totalNumber: embed.media.images.length, } } else if (AppBskyEmbedGallery.isView(embed.media)) { return { images: galleryItemsToImages(embed.media.items), totalNumber: embed.media.items.length, } } } return {images: [], totalNumber: 0} }, [embed]) return ( {sanitizeDisplayName( replyTo.author.displayName || sanitizeHandle(replyTo.author.handle), )} {replyTo.text} {images && !replyTo.moderation?.ui('contentMedia').blur && ( )} {showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && ( )} ) } function galleryItemsToImages( items: AppBskyEmbedGallery.View['items'], ): AppBskyEmbedImages.ViewImage[] { // The reply-to thumbnail only renders up to 4 tiles; slicing here keeps // the existing layout switch valid for galleries up to 10 items. return items .filter(AppBskyEmbedGallery.isViewImage) .slice(0, 4) .map(item => ({ thumb: item.thumbnail, fullsize: item.fullsize, alt: item.alt, aspectRatio: item.aspectRatio, })) } function ComposerReplyToImages({ images, totalNumber, }: { images: AppBskyEmbedImages.ViewImage[] totalNumber: number }) { const t = useTheme() return ( {(images.length === 1 && ( )) || (images.length === 2 && ( )) || (images.length === 3 && ( )) || (images.length === 4 && ( {totalNumber > 4 && ( +{totalNumber - 3} )} ))} ) }