import {memo, useCallback, useMemo, useState} from 'react' import {View} from 'react-native' import { type AppBskyFeedDefs, type AppBskyFeedThreadgate, AtUri, RichText as RichTextAPI, } from '@atproto/api' import {Trans} from '@lingui/macro' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' import { POST_TOMBSTONE, type Shadow, usePostShadow, } from '#/state/cache/post-shadow' import {type ThreadItem} from '#/state/queries/usePostThread/types' import {useSession} from '#/state/session' import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {PostMeta} from '#/view/com/util/PostMeta' import { OUTER_SPACE, REPLY_LINE_WIDTH, TREE_AVI_WIDTH, TREE_INDENT, } from '#/screens/PostThread/const' import {atoms as a, useTheme} from '#/alf' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostHider} from '#/components/moderation/PostHider' import {type AppModerationCause} from '#/components/Pills' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' import {PostControls} from '#/components/PostControls' import {RichText} from '#/components/RichText' import * as Skele from '#/components/Skeleton' import {SubtleWebHover} from '#/components/SubtleWebHover' import {Text} from '#/components/Typography' /** * Mimic the space in PostMeta */ const TREE_AVI_PLUS_SPACE = TREE_AVI_WIDTH + a.gap_xs.gap export function ThreadItemTreePost({ item, overrides, onPostSuccess, threadgateRecord, }: { item: Extract overrides?: { moderation?: boolean topBorder?: boolean } onPostSuccess?: (data: OnPostSuccessData) => void threadgateRecord?: AppBskyFeedThreadgate.Record }) { const postShadow = usePostShadow(item.value.post) if (postShadow === POST_TOMBSTONE) { return } return ( ) } function ThreadItemTreePostDeleted({ item, }: { item: Extract }) { const t = useTheme() return ( Post has been deleted {item.ui.isLastChild && !item.ui.precedesChildReadMore && ( )} ) } const ThreadItemTreePostOuterWrapper = memo( function ThreadItemTreePostOuterWrapper({ item, children, }: { item: Extract children: React.ReactNode }) { const t = useTheme() const indents = Math.max(0, item.ui.indent - 1) return ( {Array.from(Array(indents)).map((_, n: number) => { const isSkipped = item.ui.skippedIndentIndices.has(n) return ( ) })} {children} ) }, ) const ThreadItemTreePostInnerWrapper = memo( function ThreadItemTreePostInnerWrapper({ item, children, }: { item: Extract children: React.ReactNode }) { const t = useTheme() return ( {item.ui.indent > 1 && ( )} {children} ) }, ) const ThreadItemTreeReplyChildReplyLine = memo( function ThreadItemTreeReplyChildReplyLine({ item, }: { item: Extract }) { const t = useTheme() return ( {item.ui.showChildReplyLine && ( )} ) }, ) const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ item, postShadow, overrides, onPostSuccess, threadgateRecord, }: { item: Extract postShadow: Shadow overrides?: { moderation?: boolean topBorder?: boolean } onPostSuccess?: (data: OnPostSuccessData) => void threadgateRecord?: AppBskyFeedThreadgate.Record }): React.ReactNode { const {openComposer} = useOpenComposer() const {currentAccount} = useSession() const post = item.value.post const record = item.value.post.record const moderation = item.moderation const richText = useMemo( () => new RichTextAPI({ text: record.text, facets: record.facets, }), [record], ) const [limitLines, setLimitLines] = useState( () => countLines(richText?.text) >= MAX_POST_LINES, ) const threadRootUri = record.reply?.root?.uri || post.uri const postHref = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ threadgateRecord, }) const additionalPostAlerts: AppModerationCause[] = useMemo(() => { const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) const isControlledByViewer = new AtUri(threadRootUri).host === currentAccount?.did return isControlledByViewer && isPostHiddenByThreadgate ? [ { type: 'reply-hidden', source: {type: 'user', did: currentAccount?.did}, priority: 6, }, ] : [] }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri]) const onPressReply = useCallback(() => { openComposer({ replyTo: { uri: post.uri, cid: post.cid, text: record.text, author: post.author, embed: post.embed, moderation, langs: post.record.langs, }, onPostSuccess: onPostSuccess, }) }, [openComposer, post, record, onPostSuccess, moderation]) const onPressShowMore = useCallback(() => { setLimitLines(false) }, [setLimitLines]) return ( {richText?.text ? ( <> {limitLines && ( )} ) : null} {post.embed && ( )} ) }) function SubtleHover({children}: {children: React.ReactNode}) { const { state: hover, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() return ( {children} ) } export function ThreadItemTreePostSkeleton({index}: {index: number}) { const t = useTheme() const even = index % 2 === 0 return ( {even ? ( <> ) : ( )} ) }