import React, {useMemo} from 'react' import {StyleSheet, View} from 'react-native' import { AtUri, AppBskyFeedDefs, AppBskyFeedPost, RichText as RichTextAPI, moderatePost, PostModeration, } from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Link, TextLink} from '../util/Link' import {RichText} from '../util/text/RichText' import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {niceDate} from 'lib/strings/time' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {countLines, pluralize} from 'lib/strings/helpers' import {isEmbedByEmbedder} from 'lib/embeds' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' import {useStores} from 'state/index' import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/post-embeds' import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostDropdownBtn} from '../util/forms/PostDropdownBtn' import {PostHider} from '../util/moderation/PostHider' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' import {PostSandboxWarning} from '../util/PostSandboxWarning' import {ErrorMessage} from '../util/error/ErrorMessage' import {usePalette} from 'lib/hooks/usePalette' import {formatCount} from '../util/numeric/format' import {TimeElapsed} from 'view/com/util/TimeElapsed' import {makeProfileLink} from 'lib/routes/links' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {MAX_POST_LINES} from 'lib/constants' import {Trans} from '@lingui/macro' import {useLanguagePrefs} from '#/state/preferences' import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' export function PostThreadItem({ post, record, dataUpdatedAt, treeView, depth, isHighlightedPost, hasMore, showChildReplyLine, showParentReplyLine, hasPrecedingItem, onPostReply, }: { post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record dataUpdatedAt: number treeView: boolean depth: number isHighlightedPost?: boolean hasMore?: boolean showChildReplyLine?: boolean showParentReplyLine?: boolean hasPrecedingItem: boolean onPostReply: () => void }) { const store = useStores() const postShadowed = usePostShadow(post, dataUpdatedAt) const richText = useMemo( () => new RichTextAPI({ text: record.text, facets: record.facets, }), [record], ) const moderation = useMemo( () => post ? moderatePost(post, store.preferences.moderationOpts) : undefined, [post, store], ) if (postShadowed === POST_TOMBSTONE) { return } if (richText && moderation) { return ( ) } return null } function PostThreadItemDeleted() { const styles = useStyles() const pal = usePalette('default') return ( This post has been deleted. ) } function PostThreadItemLoaded({ post, record, richText, moderation, treeView, depth, isHighlightedPost, hasMore, showChildReplyLine, showParentReplyLine, hasPrecedingItem, onPostReply, }: { post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record richText: RichTextAPI moderation: PostModeration treeView: boolean depth: number isHighlightedPost?: boolean hasMore?: boolean showChildReplyLine?: boolean showParentReplyLine?: boolean hasPrecedingItem: boolean onPostReply: () => void }) { const pal = usePalette('default') const store = useStores() const langPrefs = useLanguagePrefs() const [limitLines, setLimitLines] = React.useState( countLines(richText?.text) >= MAX_POST_LINES, ) const styles = useStyles() const hasEngagement = post.likeCount || post.repostCount const rootUri = record.reply?.root?.uri || post.uri const postHref = React.useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) const itemTitle = `Post by ${post.author.handle}` const authorHref = makeProfileLink(post.author) const authorTitle = post.author.handle const isAuthorMuted = post.author.viewer?.muted const likesHref = React.useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') }, [post.uri, post.author]) const likesTitle = 'Likes on this post' const repostsHref = React.useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') }, [post.uri, post.author]) const repostsTitle = 'Reposts of this post' const translatorUrl = getTranslatorLink( record?.text || '', langPrefs.primaryLanguage, ) const needsTranslation = useMemo( () => Boolean( langPrefs.primaryLanguage && !isPostInLanguage(post, [langPrefs.primaryLanguage]), ), [post, langPrefs.primaryLanguage], ) const onPressReply = React.useCallback(() => { store.shell.openComposer({ replyTo: { uri: post.uri, cid: post.cid, text: record.text, author: { handle: post.author.handle, displayName: post.author.displayName, avatar: post.author.avatar, }, }, onPost: onPostReply, }) }, [store, post, record, onPostReply]) const onPressShowMore = React.useCallback(() => { setLimitLines(false) }, [setLimitLines]) if (!record) { return } if (isHighlightedPost) { return ( <> {rootUri !== post.uri && ( )} {sanitizeDisplayName( post.author.displayName || sanitizeHandle(post.author.handle), )} {({timeElapsed}) => ( · {timeElapsed} )} {isAuthorMuted && ( Muted )} {sanitizeHandle(post.author.handle, '@')} {richText?.text ? ( ) : undefined} {post.embed && ( )} {hasEngagement ? ( {post.repostCount ? ( {formatCount(post.repostCount)} {' '} {pluralize(post.repostCount, 'repost')} ) : ( <> )} {post.likeCount ? ( {formatCount(post.likeCount)} {' '} {pluralize(post.likeCount, 'like')} ) : ( <> )} ) : ( <> )} ) } else { const isThreadedChild = treeView && depth > 1 return ( {!isThreadedChild && showParentReplyLine && ( )} {!isThreadedChild && ( {showChildReplyLine && ( )} )} {richText?.text ? ( ) : undefined} {limitLines ? ( ) : undefined} {post.embed && ( )} {hasMore ? ( More ) : undefined} ) } } function PostOuterWrapper({ post, treeView, depth, showParentReplyLine, hasPrecedingItem, children, }: React.PropsWithChildren<{ post: AppBskyFeedDefs.PostView treeView: boolean depth: number showParentReplyLine: boolean hasPrecedingItem: boolean }>) { const {isMobile} = useWebMediaQueries() const pal = usePalette('default') const styles = useStyles() if (treeView && depth > 1) { return ( {Array.from(Array(depth - 1)).map((_, n: number) => ( ))} {children} ) } return ( {children} ) } function ExpandedPostDetails({ post, needsTranslation, translatorUrl, }: { post: AppBskyFeedDefs.PostView needsTranslation: boolean translatorUrl: string }) { const pal = usePalette('default') return ( {niceDate(post.indexedAt)} {needsTranslation && ( <> Translate )} ) } const useStyles = () => { const {isDesktop} = useWebMediaQueries() return StyleSheet.create({ outer: { borderTopWidth: 1, paddingLeft: 8, }, outerHighlighted: { paddingTop: 16, paddingLeft: 8, paddingRight: 8, }, noTopBorder: { borderTopWidth: 0, }, layout: { flexDirection: 'row', gap: 10, paddingLeft: 8, }, layoutAvi: {}, layoutContent: { flex: 1, paddingRight: 10, }, meta: { flexDirection: 'row', paddingTop: 2, paddingBottom: 2, }, metaExpandedLine1: { paddingTop: 5, paddingBottom: 0, }, metaItem: { paddingRight: 5, maxWidth: isDesktop ? 380 : 220, }, alert: { marginBottom: 6, }, postTextContainer: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', paddingBottom: 4, paddingRight: 10, }, postTextLargeContainer: { paddingHorizontal: 0, paddingBottom: 10, }, translateLink: { marginBottom: 6, }, contentHider: { marginBottom: 6, }, contentHiderChild: { marginTop: 6, }, expandedInfo: { flexDirection: 'row', padding: 10, borderTopWidth: 1, borderBottomWidth: 1, marginTop: 5, marginBottom: 15, }, expandedInfoItem: { marginRight: 10, }, loadMore: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', gap: 4, paddingHorizontal: 20, }, replyLine: { width: 2, marginLeft: 'auto', marginRight: 'auto', }, cursor: { // @ts-ignore web only cursor: 'pointer', }, }) }