import {memo, type ReactNode, 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 {useActorStatus} from '#/lib/actor-status' 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 {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import { LINEAR_AVI_WIDTH, OUTER_SPACE, REPLY_LINE_WIDTH, } 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' export type ThreadItemPostProps = { item: Extract overrides?: { moderation?: boolean topBorder?: boolean } onPostSuccess?: (data: OnPostSuccessData) => void threadgateRecord?: AppBskyFeedThreadgate.Record } export function ThreadItemPost({ item, overrides, onPostSuccess, threadgateRecord, }: ThreadItemPostProps) { const postShadow = usePostShadow(item.value.post) if (postShadow === POST_TOMBSTONE) { return } return ( ) } function ThreadItemPostDeleted({ item, overrides, }: Pick) { const t = useTheme() return ( Post has been deleted ) } const ThreadItemPostOuterWrapper = memo(function ThreadItemPostOuterWrapper({ item, overrides, children, }: Pick & { children: ReactNode }) { const t = useTheme() const showTopBorder = !item.ui.showParentReplyLine && overrides?.topBorder !== true return ( {children} ) }) /** * Provides some space between posts as well as contains the reply line */ const ThreadItemPostParentReplyLine = memo( function ThreadItemPostParentReplyLine({ item, }: Pick) { const t = useTheme() return ( {item.ui.showParentReplyLine && ( )} ) }, ) const ThreadItemPostInner = memo(function ThreadItemPostInner({ item, postShadow, overrides, onPostSuccess, threadgateRecord, }: ThreadItemPostProps & { postShadow: Shadow }) { const t = useTheme() 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, }, onPostSuccess: onPostSuccess, }) }, [openComposer, post, record, onPostSuccess, moderation]) const onPressShowMore = useCallback(() => { setLimitLines(false) }, [setLimitLines]) const {isActive: live} = useActorStatus(post.author) return ( {(item.ui.showChildReplyLine || item.ui.precedesChildReadMore) && ( )} {richText?.text ? ( <> {limitLines && ( )} ) : undefined} {post.embed && ( )} ) }) function SubtleHover({children}: {children: ReactNode}) { const { state: hover, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() return ( {children} ) } export function ThreadItemPostSkeleton({index}: {index: number}) { const even = index % 2 === 0 return ( {even ? ( <> ) : ( )} ) }