diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
deleted file mode 100644
index 679a506b90..0000000000
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ /dev/null
@@ -1,1036 +0,0 @@
-import {memo, useCallback, useMemo, useState} from 'react'
-import {
- type GestureResponderEvent,
- StyleSheet,
- Text as RNText,
- View,
-} from 'react-native'
-import {
- AppBskyFeedDefs,
- AppBskyFeedPost,
- type AppBskyFeedThreadgate,
- AtUri,
- type ModerationDecision,
- RichText as RichTextAPI,
-} from '@atproto/api'
-import {msg, Plural, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {useActorStatus} from '#/lib/actor-status'
-import {MAX_POST_LINES} from '#/lib/constants'
-import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useTranslate} from '#/lib/hooks/useTranslate'
-import {makeProfileLink} from '#/lib/routes/links'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {countLines} from '#/lib/strings/helpers'
-import {niceDate} from '#/lib/strings/time'
-import {s} from '#/lib/styles'
-import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers'
-import {logger} from '#/logger'
-import {
- POST_TOMBSTONE,
- type Shadow,
- usePostShadow,
-} from '#/state/cache/post-shadow'
-import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
-import {useLanguagePrefs} from '#/state/preferences'
-import {type ThreadPost} from '#/state/queries/post-thread'
-import {useSession} from '#/state/session'
-import {type OnPostSuccessData} from '#/state/shell/composer'
-import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {type PostSource} from '#/state/unstable-post-source'
-import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
-import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
-import {Link} from '#/view/com/util/Link'
-import {formatCount} from '#/view/com/util/numeric/format'
-import {PostMeta} from '#/view/com/util/PostMeta'
-import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, useTheme} from '#/alf'
-import {colors} from '#/components/Admonition'
-import {Button} from '#/components/Button'
-import {useInteractionState} from '#/components/hooks/useInteractionState'
-import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
-import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
-import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
-import {InlineLinkText} from '#/components/Link'
-import {ContentHider} from '#/components/moderation/ContentHider'
-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 * as Prompt from '#/components/Prompt'
-import {RichText} from '#/components/RichText'
-import {SubtleWebHover} from '#/components/SubtleWebHover'
-import {Text} from '#/components/Typography'
-import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
-import {WhoCanReply} from '#/components/WhoCanReply'
-import * as bsky from '#/types/bsky'
-
-export function PostThreadItem({
- post,
- record,
- moderation,
- treeView,
- depth,
- prevPost,
- nextPost,
- isHighlightedPost,
- hasMore,
- showChildReplyLine,
- showParentReplyLine,
- hasPrecedingItem,
- overrideBlur,
- onPostReply,
- onPostSuccess,
- hideTopBorder,
- threadgateRecord,
- anchorPostSource,
-}: {
- post: AppBskyFeedDefs.PostView
- record: AppBskyFeedPost.Record
- moderation: ModerationDecision | undefined
- treeView: boolean
- depth: number
- prevPost: ThreadPost | undefined
- nextPost: ThreadPost | undefined
- isHighlightedPost?: boolean
- hasMore?: boolean
- showChildReplyLine?: boolean
- showParentReplyLine?: boolean
- hasPrecedingItem: boolean
- overrideBlur: boolean
- onPostReply: (postUri: string | undefined) => void
- onPostSuccess?: (data: OnPostSuccessData) => void
- hideTopBorder?: boolean
- threadgateRecord?: AppBskyFeedThreadgate.Record
- anchorPostSource?: PostSource
-}) {
- const postShadowed = usePostShadow(post)
- const richText = useMemo(
- () =>
- new RichTextAPI({
- text: record.text,
- facets: record.facets,
- }),
- [record],
- )
- if (postShadowed === POST_TOMBSTONE) {
- return
- }
- if (richText && moderation) {
- return (
-
- )
- }
- return null
-}
-
-function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
- const t = useTheme()
- return (
-
-
-
- This post has been deleted.
-
-
- )
-}
-
-let PostThreadItemLoaded = ({
- post,
- record,
- richText,
- moderation,
- treeView,
- depth,
- prevPost,
- nextPost,
- isHighlightedPost,
- hasMore,
- showChildReplyLine,
- showParentReplyLine,
- hasPrecedingItem,
- overrideBlur,
- onPostReply,
- onPostSuccess,
- hideTopBorder,
- threadgateRecord,
- anchorPostSource,
-}: {
- post: Shadow
- record: AppBskyFeedPost.Record
- richText: RichTextAPI
- moderation: ModerationDecision
- treeView: boolean
- depth: number
- prevPost: ThreadPost | undefined
- nextPost: ThreadPost | undefined
- isHighlightedPost?: boolean
- hasMore?: boolean
- showChildReplyLine?: boolean
- showParentReplyLine?: boolean
- hasPrecedingItem: boolean
- overrideBlur: boolean
- onPostReply: (postUri: string | undefined) => void
- onPostSuccess?: (data: OnPostSuccessData) => void
- hideTopBorder?: boolean
- threadgateRecord?: AppBskyFeedThreadgate.Record
- anchorPostSource?: PostSource
-}): React.ReactNode => {
- const {currentAccount, hasSession} = useSession()
- const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
-
- const t = useTheme()
- const pal = usePalette('default')
- const {_, i18n} = useLingui()
- const langPrefs = useLanguagePrefs()
- const {openComposer} = useOpenComposer()
- const [limitLines, setLimitLines] = useState(
- () => countLines(richText?.text) >= MAX_POST_LINES,
- )
- const shadowedPostAuthor = useProfileShadow(post.author)
- const rootUri = 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 itemTitle = _(msg`Post by ${post.author.handle}`)
- const authorHref = makeProfileLink(post.author)
- const authorTitle = post.author.handle
- const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
- const likesHref = useMemo(() => {
- const urip = new AtUri(post.uri)
- return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
- }, [post.uri, post.author])
- const likesTitle = _(msg`Likes on this post`)
- const repostsHref = useMemo(() => {
- const urip = new AtUri(post.uri)
- return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
- }, [post.uri, post.author])
- const repostsTitle = _(msg`Reposts of this post`)
- const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
- threadgateRecord,
- })
- const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
- const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
- const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did
- return isControlledByViewer && isPostHiddenByThreadgate
- ? [
- {
- type: 'reply-hidden',
- source: {type: 'user', did: currentAccount?.did},
- priority: 6,
- },
- ]
- : []
- }, [post, currentAccount?.did, threadgateHiddenReplies, rootUri])
- const quotesHref = useMemo(() => {
- const urip = new AtUri(post.uri)
- return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
- }, [post.uri, post.author])
- const quotesTitle = _(msg`Quotes of this post`)
- const onlyFollowersCanReply = !!threadgateRecord?.allow?.find(
- rule => rule.$type === 'app.bsky.feed.threadgate#followerRule',
- )
- const showFollowButton =
- currentAccount?.did !== post.author.did && !onlyFollowersCanReply
-
- const needsTranslation = useMemo(
- () =>
- Boolean(
- langPrefs.primaryLanguage &&
- !isPostInLanguage(post, [langPrefs.primaryLanguage]),
- ),
- [post, langPrefs.primaryLanguage],
- )
-
- const onPressReply = () => {
- if (anchorPostSource && isHighlightedPost) {
- feedFeedback.sendInteraction({
- item: post.uri,
- event: 'app.bsky.feed.defs#interactionReply',
- feedContext: anchorPostSource.post.feedContext,
- reqId: anchorPostSource.post.reqId,
- })
- }
- openComposer({
- replyTo: {
- uri: post.uri,
- cid: post.cid,
- text: record.text,
- author: post.author,
- embed: post.embed,
- moderation,
- langs: record.langs,
- },
- onPost: onPostReply,
- onPostSuccess: onPostSuccess,
- })
- }
-
- const onOpenAuthor = () => {
- if (anchorPostSource) {
- feedFeedback.sendInteraction({
- item: post.uri,
- event: 'app.bsky.feed.defs#clickthroughAuthor',
- feedContext: anchorPostSource.post.feedContext,
- reqId: anchorPostSource.post.reqId,
- })
- }
- }
-
- const onOpenEmbed = () => {
- if (anchorPostSource) {
- feedFeedback.sendInteraction({
- item: post.uri,
- event: 'app.bsky.feed.defs#clickthroughEmbed',
- feedContext: anchorPostSource.post.feedContext,
- reqId: anchorPostSource.post.reqId,
- })
- }
- }
-
- const onPressShowMore = useCallback(() => {
- setLimitLines(false)
- }, [setLimitLines])
-
- const {isActive: live} = useActorStatus(post.author)
-
- const reason = anchorPostSource?.post.reason
- const viaRepost = useMemo(() => {
- if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) {
- return {
- uri: reason.uri,
- cid: reason.cid,
- }
- }
- }, [reason])
-
- if (!record) {
- return
- }
-
- if (isHighlightedPost) {
- return (
- <>
- {rootUri !== post.uri && (
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
- {sanitizeDisplayName(
- post.author.displayName ||
- sanitizeHandle(post.author.handle),
- moderation.ui('displayName'),
- )}
-
-
-
-
-
-
-
-
-
- {sanitizeHandle(post.author.handle, '@')}
-
-
-
- {showFollowButton && (
-
-
-
- )}
-
-
-
-
-
- {richText?.text ? (
-
- ) : undefined}
- {post.embed && (
-
-
-
- )}
-
-
- {post.repostCount !== 0 ||
- post.likeCount !== 0 ||
- post.quoteCount !== 0 ? (
- // Show this section unless we're *sure* it has no engagement.
-
- {post.repostCount != null && post.repostCount !== 0 ? (
-
-
-
- {formatCount(i18n, post.repostCount)}
- {' '}
-
-
-
- ) : null}
- {post.quoteCount != null &&
- post.quoteCount !== 0 &&
- !post.viewer?.embeddingDisabled ? (
-
-
-
- {formatCount(i18n, post.quoteCount)}
- {' '}
-
-
-
- ) : null}
- {post.likeCount != null && post.likeCount !== 0 ? (
-
-
-
- {formatCount(i18n, post.likeCount)}
- {' '}
-
-
-
- ) : null}
-
- ) : null}
-
-
-
-
-
-
-
- >
- )
- } else {
- const isThreadedChild = treeView && depth > 0
- const isThreadedChildAdjacentTop =
- isThreadedChild && prevPost?.ctx.depth === depth && depth !== 1
- const isThreadedChildAdjacentBot =
- isThreadedChild && nextPost?.ctx.depth === depth
- return (
-
-
-
-
- {!isThreadedChild && showParentReplyLine && (
-
- )}
-
-
-
-
- {/* If we are in threaded mode, the avatar is rendered in PostMeta */}
- {!isThreadedChild && (
-
-
-
- {showChildReplyLine && (
-
- )}
-
- )}
-
-
-
-
-
- {richText?.text ? (
-
-
- {limitLines && (
-
- )}
-
- ) : undefined}
- {post.embed && (
-
-
-
- )}
-
-
-
- {hasMore ? (
-
-
- More
-
-
-
- ) : undefined}
-
-
- )
- }
-}
-PostThreadItemLoaded = memo(PostThreadItemLoaded)
-
-function PostOuterWrapper({
- post,
- treeView,
- depth,
- showParentReplyLine,
- hasPrecedingItem,
- hideTopBorder,
- children,
-}: React.PropsWithChildren<{
- post: AppBskyFeedDefs.PostView
- treeView: boolean
- depth: number
- showParentReplyLine: boolean
- hasPrecedingItem: boolean
- hideTopBorder?: boolean
-}>) {
- const t = useTheme()
- const {
- state: hover,
- onIn: onHoverIn,
- onOut: onHoverOut,
- } = useInteractionState()
- if (treeView && depth > 0) {
- return (
-
- {Array.from(Array(depth - 1)).map((_, n: number) => (
-
- ))}
-
-
- {children}
-
-
- )
- }
- return (
-
-
- {children}
-
- )
-}
-
-function ExpandedPostDetails({
- post,
- record,
- isThreadAuthor,
- needsTranslation,
-}: {
- post: AppBskyFeedDefs.PostView
- record: AppBskyFeedPost.Record
- isThreadAuthor: boolean
- needsTranslation: boolean
-}) {
- const t = useTheme()
- const pal = usePalette('default')
- const {_, i18n} = useLingui()
- const translate = useTranslate()
- const isRootPost = !('reply' in post.record)
- const langPrefs = useLanguagePrefs()
-
- const onTranslatePress = useCallback(
- (e: GestureResponderEvent) => {
- e.preventDefault()
- translate(record.text || '', langPrefs.primaryLanguage)
-
- if (
- bsky.dangerousIsType(
- post.record,
- AppBskyFeedPost.isRecord,
- )
- ) {
- logger.metric(
- 'translate',
- {
- sourceLanguages: post.record.langs ?? [],
- targetLanguage: langPrefs.primaryLanguage,
- textLength: post.record.text.length,
- },
- {statsig: false},
- )
- }
-
- return false
- },
- [translate, record.text, langPrefs, post],
- )
-
- return (
-
-
-
-
- {niceDate(i18n, post.indexedAt)}
-
- {isRootPost && (
-
- )}
- {needsTranslation && (
- <>
-
- ·
-
-
-
- Translate
-
- >
- )}
-
-
- )
-}
-
-function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
- const t = useTheme()
- const {_, i18n} = useLingui()
- const control = Prompt.usePromptControl()
-
- const indexedAt = new Date(post.indexedAt)
- const createdAt = bsky.dangerousIsType(
- post.record,
- AppBskyFeedPost.isRecord,
- )
- ? new Date(post.record.createdAt)
- : new Date(post.indexedAt)
-
- // backdated if createdAt is 24 hours or more before indexedAt
- const isBackdated =
- indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000
-
- if (!isBackdated) return null
-
- const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light
-
- return (
- <>
-
-
-
-
- Archived post
-
-
-
- This post claims to have been created on{' '}
- {niceDate(i18n, createdAt)},
- but was first seen by Bluesky on{' '}
- {niceDate(i18n, indexedAt)}.
-
-
-
-
- Bluesky cannot confirm the authenticity of the claimed date.
-
-
-
- {}} />
-
-
- >
- )
-}
-
-function getThreadAuthor(
- post: AppBskyFeedDefs.PostView,
- record: AppBskyFeedPost.Record,
-): string {
- if (!record.reply) {
- return post.author.did
- }
- try {
- return new AtUri(record.reply.root.uri).host
- } catch {
- return ''
- }
-}
-
-const styles = StyleSheet.create({
- outer: {
- borderTopWidth: StyleSheet.hairlineWidth,
- paddingLeft: 8,
- },
- noTopBorder: {
- borderTopWidth: 0,
- },
- meta: {
- flexDirection: 'row',
- paddingVertical: 2,
- },
- metaExpandedLine1: {
- paddingVertical: 0,
- },
- 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',
- },
-})
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
index d2e492f7e3..ecee5a0ae9 100644
--- a/src/view/screens/DebugMod.tsx
+++ b/src/view/screens/DebugMod.tsx
@@ -31,8 +31,11 @@ import {
groupNotifications,
shouldFilterNotif,
} from '#/state/queries/notifications/util'
+import {threadPost} from '#/state/queries/usePostThread/views'
import {useSession} from '#/state/session'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
+import {ThreadItemAnchor} from '#/screens/PostThread/components/ThreadItemAnchor'
+import {ThreadItemPost} from '#/screens/PostThread/components/ThreadItemPost'
import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -49,7 +52,6 @@ import * as ProfileCard from '#/components/ProfileCard'
import {H1, H3, P, Text} from '#/components/Typography'
import {ScreenHider} from '../../components/moderation/ScreenHider'
import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem'
-import {PostThreadItem} from '../com/post-thread/PostThreadItem'
import {PostFeedItem} from '../com/posts/PostFeedItem'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
@@ -519,13 +521,13 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
-
+
-
>
)}
@@ -835,30 +837,35 @@ function MockPostFeedItem({
)
}
-function MockPostThreadItem({
+function MockPostThreadItemV2({
post,
- moderation,
- reply,
+ moderationOpts,
+ isReply,
}: {
post: AppBskyFeedDefs.PostView
- moderation: ModerationDecision
- reply?: boolean
+ moderationOpts: ModerationOpts
+ isReply?: boolean
}) {
- return (
- {}}
- />
+ const thread = threadPost({
+ uri: post.uri,
+ depth: isReply ? 1 : 0,
+ value: {
+ $type: 'app.bsky.unspecced.defs#threadItemPost',
+ post,
+ moreParents: false,
+ moreReplies: 0,
+ opThread: false,
+ hiddenByThreadgate: false,
+ mutedByViewer: false,
+ },
+ moderationOpts,
+ threadgateHiddenReplies: new Set(),
+ })
+
+ return isReply ? (
+
+ ) : (
+
)
}