about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-08-26 09:54:19 -0500
committerGitHub <noreply@github.com>2025-08-26 09:54:19 -0500
commitdf20ae237eaf434c6ed0fd032f8328cd9b8c352c (patch)
treeeecd070cf125acc908b1137a569aa369fe5fc436
parente91a6838101c9566ce2dafaa6fe8c77293a5eba6 (diff)
downloadvoidsky-df20ae237eaf434c6ed0fd032f8328cd9b8c352c.tar.zst
Threads v2 cleanup (#8902)
* Delete root PostThread component

* Remove PostThreadItem, migrate DebugMod to use new components

* Remove other unused components

* Move PostThreadFollowBtn to new home

* Move PostThreadComposePrompt to new home

* Remove gate

* Keep naming in DebugMod

* rm v1 prefs

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
-rw-r--r--src/lib/statsig/gates.ts1
-rw-r--r--src/screens/PostThread/components/ThreadComposePrompt.tsx (renamed from src/view/com/post-thread/PostThreadComposePrompt.tsx)2
-rw-r--r--src/screens/PostThread/components/ThreadItemAnchor.tsx4
-rw-r--r--src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx (renamed from src/view/com/post-thread/PostThreadFollowBtn.tsx)2
-rw-r--r--src/screens/PostThread/index.tsx6
-rw-r--r--src/screens/Settings/ThreadPreferences.tsx158
-rw-r--r--src/screens/VideoFeed/index.tsx4
-rw-r--r--src/view/com/post-thread/PostThread.tsx910
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx1036
-rw-r--r--src/view/com/post-thread/PostThreadLoadMore.tsx65
-rw-r--r--src/view/com/post-thread/PostThreadShowHiddenReplies.tsx62
-rw-r--r--src/view/screens/DebugMod.tsx53
-rw-r--r--src/view/screens/PostThread.tsx9
13 files changed, 40 insertions, 2272 deletions
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index ef6dc1d4d..90ccd3443 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -8,7 +8,6 @@ export type Gate =
   | 'old_postonboarding'
   | 'onboarding_add_video_feed'
   | 'post_follow_profile_suggested_accounts'
-  | 'post_threads_v2_unspecced'
   | 'remove_show_latest_button'
   | 'test_gate_1'
   | 'test_gate_2'
diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/screens/PostThread/components/ThreadComposePrompt.tsx
index dc0561725..e12c7e766 100644
--- a/src/view/com/post-thread/PostThreadComposePrompt.tsx
+++ b/src/screens/PostThread/components/ThreadComposePrompt.tsx
@@ -14,7 +14,7 @@ import {transparentifyColor} from '#/alf/util/colorGeneration'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {Text} from '#/components/Typography'
 
-export function PostThreadComposePrompt({
+export function ThreadComposePrompt({
   onPressCompose,
   style,
 }: {
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index fc1f1caeb..550bddc6a 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -32,9 +32,9 @@ 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 {formatCount} from '#/view/com/util/numeric/format'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
+import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
 import {
   LINEAR_AVI_WIDTH,
   OUTER_SPACE,
@@ -367,7 +367,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
           </Link>
           {showFollowButton && (
             <View collapsable={false}>
-              <PostThreadFollowBtn did={post.author.did} />
+              <ThreadItemAnchorFollowButton did={post.author.did} />
             </View>
           )}
         </View>
diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
index fc9296cad..d4cf120cf 100644
--- a/src/view/com/post-thread/PostThreadFollowBtn.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
@@ -17,7 +17,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
 
-export function PostThreadFollowBtn({did}: {did: string}) {
+export function ThreadItemAnchorFollowButton({did}: {did: string}) {
   const {data: profile, isLoading} = useProfileQuery({did})
 
   // We will never hit this - the profile will always be cached or loaded above
diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx
index f91daf54b..7432f71db 100644
--- a/src/screens/PostThread/index.tsx
+++ b/src/screens/PostThread/index.tsx
@@ -12,9 +12,9 @@ import {useSession} from '#/state/session'
 import {type OnPostSuccessData} from '#/state/shell/composer'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {useUnstablePostSource} from '#/state/unstable-post-source'
-import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
 import {List, type ListMethods} from '#/view/com/util/List'
 import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown'
+import {ThreadComposePrompt} from '#/screens/PostThread/components/ThreadComposePrompt'
 import {ThreadError} from '#/screens/PostThread/components/ThreadError'
 import {
   ThreadItemAnchor,
@@ -455,7 +455,7 @@ export function PostThread({uri}: {uri: string}) {
         return (
           <View>
             {gtMobile && (
-              <PostThreadComposePrompt onPressCompose={onReplyToAnchor} />
+              <ThreadComposePrompt onPressCompose={onReplyToAnchor} />
             )}
           </View>
         )
@@ -586,7 +586,7 @@ function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
 
   return (
     <Animated.View style={[a.fixed, a.left_0, a.right_0, animatedStyle]}>
-      <PostThreadComposePrompt onPressCompose={onPressReply} />
+      <ThreadComposePrompt onPressCompose={onPressReply} />
     </Animated.View>
   )
 }
diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx
index af3cf915f..cba896a76 100644
--- a/src/screens/Settings/ThreadPreferences.tsx
+++ b/src/screens/Settings/ThreadPreferences.tsx
@@ -6,11 +6,6 @@ import {
   type CommonNavigatorParams,
   type NativeStackScreenProps,
 } from '#/lib/routes/types'
-import {useGate} from '#/lib/statsig/statsig'
-import {
-  usePreferencesQuery,
-  useSetThreadViewPreferencesMutation,
-} from '#/state/queries/preferences'
 import {
   normalizeSort,
   normalizeView,
@@ -18,7 +13,6 @@ import {
 } from '#/state/queries/preferences/useThreadPreferences'
 import {atoms as a, useTheme} from '#/alf'
 import * as Toggle from '#/components/forms/Toggle'
-import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
 import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
 import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
 import {Tree_Stroke2_Corner0_Rounded as TreeIcon} from '#/components/icons/Tree'
@@ -28,16 +22,6 @@ import * as SettingsList from './components/SettingsList'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
 export function ThreadPreferencesScreen({}: Props) {
-  const gate = useGate()
-
-  return gate('post_threads_v2_unspecced') ? (
-    <ThreadPreferencesV2 />
-  ) : (
-    <ThreadPreferencesV1 />
-  )
-}
-
-export function ThreadPreferencesV2() {
   const t = useTheme()
   const {_} = useLingui()
   const {
@@ -150,145 +134,3 @@ export function ThreadPreferencesV2() {
     </Layout.Screen>
   )
 }
-
-export function ThreadPreferencesV1() {
-  const {_} = useLingui()
-  const t = useTheme()
-
-  const {data: preferences} = usePreferencesQuery()
-  const {mutate: setThreadViewPrefs, variables} =
-    useSetThreadViewPreferencesMutation()
-
-  const sortReplies = variables?.sort ?? preferences?.threadViewPrefs?.sort
-
-  const prioritizeFollowedUsers = Boolean(
-    variables?.prioritizeFollowedUsers ??
-      preferences?.threadViewPrefs?.prioritizeFollowedUsers,
-  )
-  const treeViewEnabled = Boolean(
-    variables?.lab_treeViewEnabled ??
-      preferences?.threadViewPrefs?.lab_treeViewEnabled,
-  )
-
-  return (
-    <Layout.Screen testID="threadPreferencesScreen">
-      <Layout.Header.Outer>
-        <Layout.Header.BackButton />
-        <Layout.Header.Content>
-          <Layout.Header.TitleText>
-            <Trans>Thread Preferences</Trans>
-          </Layout.Header.TitleText>
-        </Layout.Header.Content>
-        <Layout.Header.Slot />
-      </Layout.Header.Outer>
-      <Layout.Content>
-        <SettingsList.Container>
-          <SettingsList.Group>
-            <SettingsList.ItemIcon icon={BubblesIcon} />
-            <SettingsList.ItemText>
-              <Trans>Sort replies</Trans>
-            </SettingsList.ItemText>
-            <View style={[a.w_full, a.gap_md]}>
-              <Text style={[a.flex_1, t.atoms.text_contrast_medium]}>
-                <Trans>Sort replies to the same post by:</Trans>
-              </Text>
-              <Toggle.Group
-                label={_(msg`Sort replies by`)}
-                type="radio"
-                values={sortReplies ? [sortReplies] : []}
-                onChange={values => setThreadViewPrefs({sort: values[0]})}>
-                <View style={[a.gap_sm, a.flex_1]}>
-                  <Toggle.Item name="hotness" label={_(msg`Hot replies first`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Hot replies first</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                  <Toggle.Item
-                    name="oldest"
-                    label={_(msg`Oldest replies first`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Oldest replies first</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                  <Toggle.Item
-                    name="newest"
-                    label={_(msg`Newest replies first`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Newest replies first</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                  <Toggle.Item
-                    name="most-likes"
-                    label={_(msg`Most-liked replies first`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Most-liked first</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                  <Toggle.Item
-                    name="random"
-                    label={_(msg`Random (aka "Poster's Roulette")`)}>
-                    <Toggle.Radio />
-                    <Toggle.LabelText>
-                      <Trans>Random (aka "Poster's Roulette")</Trans>
-                    </Toggle.LabelText>
-                  </Toggle.Item>
-                </View>
-              </Toggle.Group>
-            </View>
-          </SettingsList.Group>
-          <SettingsList.Group>
-            <SettingsList.ItemIcon icon={PersonGroupIcon} />
-            <SettingsList.ItemText>
-              <Trans>Prioritize your Follows</Trans>
-            </SettingsList.ItemText>
-            <Toggle.Item
-              type="checkbox"
-              name="prioritize-follows"
-              label={_(msg`Prioritize your Follows`)}
-              value={prioritizeFollowedUsers}
-              onChange={value =>
-                setThreadViewPrefs({
-                  prioritizeFollowedUsers: value,
-                })
-              }
-              style={[a.w_full, a.gap_md]}>
-              <Toggle.LabelText style={[a.flex_1]}>
-                <Trans>
-                  Show replies by people you follow before all other replies
-                </Trans>
-              </Toggle.LabelText>
-              <Toggle.Platform />
-            </Toggle.Item>
-          </SettingsList.Group>
-          <SettingsList.Divider />
-          <SettingsList.Group>
-            <SettingsList.ItemIcon icon={BeakerIcon} />
-            <SettingsList.ItemText>
-              <Trans>Experimental</Trans>
-            </SettingsList.ItemText>
-            <Toggle.Item
-              type="checkbox"
-              name="threaded-mode"
-              label={_(msg`Threaded mode`)}
-              value={treeViewEnabled}
-              onChange={value =>
-                setThreadViewPrefs({
-                  lab_treeViewEnabled: value,
-                })
-              }
-              style={[a.w_full, a.gap_md]}>
-              <Toggle.LabelText style={[a.flex_1]}>
-                <Trans>Show replies as threaded</Trans>
-              </Toggle.LabelText>
-              <Toggle.Platform />
-            </Toggle.Item>
-          </SettingsList.Group>
-        </SettingsList.Container>
-      </Layout.Content>
-    </Layout.Screen>
-  )
-}
diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx
index b53593010..22989e6c2 100644
--- a/src/screens/VideoFeed/index.tsx
+++ b/src/screens/VideoFeed/index.tsx
@@ -80,9 +80,9 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
-import {PostThreadComposePrompt} from '#/view/com/post-thread/PostThreadComposePrompt'
 import {List} from '#/view/com/util/List'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {ThreadComposePrompt} from '#/screens/PostThread/components/ThreadComposePrompt'
 import {Header} from '#/screens/VideoFeed/components/Header'
 import {atoms as a, ios, platform, ThemeProvider, useTheme} from '#/alf'
 import {setSystemUITheme} from '#/alf/util/systemUI'
@@ -883,7 +883,7 @@ function Overlay({
               player={player}
               seekingAnimationSV={seekingAnimationSV}
               scrollGesture={scrollGesture}>
-              <PostThreadComposePrompt
+              <ThreadComposePrompt
                 onPressCompose={onPressReply}
                 style={[a.pt_md, a.pb_sm]}
               />
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
deleted file mode 100644
index bbf9f4a20..000000000
--- a/src/view/com/post-thread/PostThread.tsx
+++ /dev/null
@@ -1,910 +0,0 @@
-import React, {memo, useRef, useState} from 'react'
-import {useWindowDimensions, View} from 'react-native'
-import {runOnJS, useAnimatedStyle} from 'react-native-reanimated'
-import Animated from 'react-native-reanimated'
-import {
-  AppBskyFeedDefs,
-  type AppBskyFeedThreadgate,
-  moderatePost,
-} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {HITSLOP_10} from '#/lib/constants'
-import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
-import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {useSetTitle} from '#/lib/hooks/useSetTitle'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {ScrollProvider} from '#/lib/ScrollContext'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {cleanError} from '#/lib/strings/errors'
-import {isAndroid, isNative, isWeb} from '#/platform/detection'
-import {useFeedFeedback} from '#/state/feed-feedback'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {
-  fillThreadModerationCache,
-  sortThread,
-  type ThreadBlocked,
-  type ThreadModerationCache,
-  type ThreadNode,
-  type ThreadNotFound,
-  type ThreadPost,
-  usePostThreadQuery,
-} from '#/state/queries/post-thread'
-import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences'
-import {usePreferencesQuery} from '#/state/queries/preferences'
-import {useSession} from '#/state/session'
-import {useShellLayout} from '#/state/shell/shell-layout'
-import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {useUnstablePostSource} from '#/state/unstable-post-source'
-import {List, type ListMethods} from '#/view/com/util/List'
-import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonIcon} from '#/components/Button'
-import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
-import {Header} from '#/components/Layout'
-import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
-import * as Menu from '#/components/Menu'
-import {Text} from '#/components/Typography'
-import {PostThreadComposePrompt} from './PostThreadComposePrompt'
-import {PostThreadItem} from './PostThreadItem'
-import {PostThreadLoadMore} from './PostThreadLoadMore'
-import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies'
-
-// FlatList maintainVisibleContentPosition breaks if too many items
-// are prepended. This seems to be an optimal number based on *shrug*.
-const PARENTS_CHUNK_SIZE = 15
-
-const MAINTAIN_VISIBLE_CONTENT_POSITION = {
-  // We don't insert any elements before the root row while loading.
-  // So the row we want to use as the scroll anchor is the first row.
-  minIndexForVisible: 0,
-}
-
-const REPLY_PROMPT = {_reactKey: '__reply__'}
-const LOAD_MORE = {_reactKey: '__load_more__'}
-const SHOW_HIDDEN_REPLIES = {_reactKey: '__show_hidden_replies__'}
-const SHOW_MUTED_REPLIES = {_reactKey: '__show_muted_replies__'}
-
-enum HiddenRepliesState {
-  Hide,
-  Show,
-  ShowAndOverridePostHider,
-}
-
-type YieldedItem =
-  | ThreadPost
-  | ThreadBlocked
-  | ThreadNotFound
-  | typeof SHOW_HIDDEN_REPLIES
-  | typeof SHOW_MUTED_REPLIES
-type RowItem =
-  | YieldedItem
-  // TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape.
-  | typeof REPLY_PROMPT
-  | typeof LOAD_MORE
-
-type ThreadSkeletonParts = {
-  parents: YieldedItem[]
-  highlightedPost: ThreadNode
-  replies: YieldedItem[]
-}
-
-const keyExtractor = (item: RowItem) => {
-  return item._reactKey
-}
-
-export function PostThread({uri}: {uri: string}) {
-  const {hasSession, currentAccount} = useSession()
-  const {_} = useLingui()
-  const t = useTheme()
-  const {isMobile} = useWebMediaQueries()
-  const initialNumToRender = useInitialNumToRender()
-  const {height: windowHeight} = useWindowDimensions()
-  const [hiddenRepliesState, setHiddenRepliesState] = React.useState(
-    HiddenRepliesState.Hide,
-  )
-  const headerRef = React.useRef<View | null>(null)
-  const anchorPostSource = useUnstablePostSource(uri)
-  const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession)
-
-  const {data: preferences} = usePreferencesQuery()
-  const {
-    isFetching,
-    isError: isThreadError,
-    error: threadError,
-    refetch,
-    data: {thread, threadgate} = {},
-    dataUpdatedAt: fetchedAt,
-  } = usePostThreadQuery(uri)
-
-  // The original source of truth for these are the server settings.
-  const serverPrefs = preferences?.threadViewPrefs
-  const serverPrioritizeFollowedUsers =
-    serverPrefs?.prioritizeFollowedUsers ?? true
-  const serverTreeViewEnabled = serverPrefs?.lab_treeViewEnabled ?? false
-  const serverSortReplies = serverPrefs?.sort ?? 'hotness'
-
-  // However, we also need these to work locally for PWI (without persistence).
-  // So we're mirroring them locally.
-  const prioritizeFollowedUsers = serverPrioritizeFollowedUsers
-  const [treeViewEnabled, setTreeViewEnabled] = useState(serverTreeViewEnabled)
-  const [sortReplies, setSortReplies] = useState(serverSortReplies)
-
-  // We'll reset the local state if new server state flows down to us.
-  const [prevServerPrefs, setPrevServerPrefs] = useState(serverPrefs)
-  if (prevServerPrefs !== serverPrefs) {
-    setPrevServerPrefs(serverPrefs)
-    setTreeViewEnabled(serverTreeViewEnabled)
-    setSortReplies(serverSortReplies)
-  }
-
-  // And we'll update the local state when mutating the server prefs.
-  const {mutate: mutateThreadViewPrefs} = useSetThreadViewPreferencesMutation()
-  function updateTreeViewEnabled(newTreeViewEnabled: boolean) {
-    setTreeViewEnabled(newTreeViewEnabled)
-    if (hasSession) {
-      mutateThreadViewPrefs({lab_treeViewEnabled: newTreeViewEnabled})
-    }
-  }
-  function updateSortReplies(newSortReplies: string) {
-    setSortReplies(newSortReplies)
-    if (hasSession) {
-      mutateThreadViewPrefs({sort: newSortReplies})
-    }
-  }
-
-  const treeView = React.useMemo(
-    () => treeViewEnabled && hasBranchingReplies(thread),
-    [treeViewEnabled, thread],
-  )
-
-  const rootPost = thread?.type === 'post' ? thread.post : undefined
-  const rootPostRecord = thread?.type === 'post' ? thread.record : undefined
-  const threadgateRecord = threadgate?.record as
-    | AppBskyFeedThreadgate.Record
-    | undefined
-  const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
-    threadgateRecord,
-  })
-
-  const moderationOpts = useModerationOpts()
-  const isNoPwi = React.useMemo(() => {
-    const mod =
-      rootPost && moderationOpts
-        ? moderatePost(rootPost, moderationOpts)
-        : undefined
-    return !!mod
-      ?.ui('contentList')
-      .blurs.find(
-        cause =>
-          cause.type === 'label' &&
-          cause.labelDef.identifier === '!no-unauthenticated',
-      )
-  }, [rootPost, moderationOpts])
-
-  // Values used for proper rendering of parents
-  const ref = useRef<ListMethods>(null)
-  const highlightedPostRef = useRef<View | null>(null)
-  const [maxParents, setMaxParents] = React.useState(
-    isWeb ? Infinity : PARENTS_CHUNK_SIZE,
-  )
-  const [maxReplies, setMaxReplies] = React.useState(50)
-
-  useSetTitle(
-    rootPost && !isNoPwi
-      ? `${sanitizeDisplayName(
-          rootPost.author.displayName || `@${rootPost.author.handle}`,
-        )}: "${rootPostRecord!.text}"`
-      : '',
-  )
-
-  // On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed.
-  // This ensures that the first render contains no parents--even if they are already available in the cache.
-  // We need to delay showing them so that we can use maintainVisibleContentPosition to keep the main post on screen.
-  // On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead.
-  const [deferParents, setDeferParents] = React.useState(isNative)
-
-  const currentDid = currentAccount?.did
-  const threadModerationCache = React.useMemo(() => {
-    const cache: ThreadModerationCache = new WeakMap()
-    if (thread && moderationOpts) {
-      fillThreadModerationCache(cache, thread, moderationOpts)
-    }
-    return cache
-  }, [thread, moderationOpts])
-
-  const [justPostedUris, setJustPostedUris] = React.useState(
-    () => new Set<string>(),
-  )
-
-  const [fetchedAtCache] = React.useState(() => new Map<string, number>())
-  const [randomCache] = React.useState(() => new Map<string, number>())
-  const skeleton = React.useMemo(() => {
-    if (!thread) return null
-    return createThreadSkeleton(
-      sortThread(
-        thread,
-        {
-          // Prefer local state as the source of truth.
-          sort: sortReplies,
-          lab_treeViewEnabled: treeViewEnabled,
-          prioritizeFollowedUsers,
-        },
-        threadModerationCache,
-        currentDid,
-        justPostedUris,
-        threadgateHiddenReplies,
-        fetchedAtCache,
-        fetchedAt,
-        randomCache,
-      ),
-      currentDid,
-      treeView,
-      threadModerationCache,
-      hiddenRepliesState !== HiddenRepliesState.Hide,
-      threadgateHiddenReplies,
-    )
-  }, [
-    thread,
-    prioritizeFollowedUsers,
-    sortReplies,
-    treeViewEnabled,
-    currentDid,
-    treeView,
-    threadModerationCache,
-    hiddenRepliesState,
-    justPostedUris,
-    threadgateHiddenReplies,
-    fetchedAtCache,
-    fetchedAt,
-    randomCache,
-  ])
-
-  const error = React.useMemo(() => {
-    if (AppBskyFeedDefs.isNotFoundPost(thread)) {
-      return {
-        title: _(msg`Post not found`),
-        message: _(msg`The post may have been deleted.`),
-      }
-    } else if (skeleton?.highlightedPost.type === 'blocked') {
-      return {
-        title: _(msg`Post hidden`),
-        message: _(
-          msg`You have blocked the author or you have been blocked by the author.`,
-        ),
-      }
-    } else if (threadError?.message.startsWith('Post not found')) {
-      return {
-        title: _(msg`Post not found`),
-        message: _(msg`The post may have been deleted.`),
-      }
-    } else if (isThreadError) {
-      return {
-        message: threadError ? cleanError(threadError) : undefined,
-      }
-    }
-
-    return null
-  }, [thread, skeleton?.highlightedPost, isThreadError, _, threadError])
-
-  // construct content
-  const posts = React.useMemo(() => {
-    if (!skeleton) return []
-
-    const {parents, highlightedPost, replies} = skeleton
-    let arr: RowItem[] = []
-    if (highlightedPost.type === 'post') {
-      // We want to wait for parents to load before rendering.
-      // If you add something here, you'll need to update both
-      // maintainVisibleContentPosition and onContentSizeChange
-      // to "hold onto" the correct row instead of the first one.
-
-      /*
-       * This is basically `!!parents.length`, see notes on `isParentLoading`
-       */
-      if (!highlightedPost.ctx.isParentLoading && !deferParents) {
-        // When progressively revealing parents, rendering a placeholder
-        // here will cause scrolling jumps. Don't add it unless you test it.
-        // QT'ing this thread is a great way to test all the scrolling hacks:
-        // https://bsky.app/profile/samuel.bsky.team/post/3kjqhblh6qk2o
-
-        // Everything is loaded
-        let startIndex = Math.max(0, parents.length - maxParents)
-        for (let i = startIndex; i < parents.length; i++) {
-          arr.push(parents[i])
-        }
-      }
-      arr.push(highlightedPost)
-      if (!highlightedPost.post.viewer?.replyDisabled) {
-        arr.push(REPLY_PROMPT)
-      }
-      for (let i = 0; i < replies.length; i++) {
-        arr.push(replies[i])
-        if (i === maxReplies) {
-          break
-        }
-      }
-    }
-    return arr
-  }, [skeleton, deferParents, maxParents, maxReplies])
-
-  // This is only used on the web to keep the post in view when its parents load.
-  // On native, we rely on `maintainVisibleContentPosition` instead.
-  const didAdjustScrollWeb = useRef<boolean>(false)
-  const onContentSizeChangeWeb = React.useCallback(() => {
-    // only run once
-    if (didAdjustScrollWeb.current) {
-      return
-    }
-    // wait for loading to finish
-    if (thread?.type === 'post' && !!thread.parent) {
-      // Measure synchronously to avoid a layout jump.
-      const postNode = highlightedPostRef.current
-      const headerNode = headerRef.current
-      if (postNode && headerNode) {
-        let pageY = (postNode as any as Element).getBoundingClientRect().top
-        pageY -= (headerNode as any as Element).getBoundingClientRect().height
-        pageY = Math.max(0, pageY)
-        ref.current?.scrollToOffset({
-          animated: false,
-          offset: pageY,
-        })
-      }
-      didAdjustScrollWeb.current = true
-    }
-  }, [thread])
-
-  // On native, we reveal parents in chunks. Although they're all already
-  // loaded and FlatList already has its own virtualization, unfortunately FlatList
-  // has a bug that causes the content to jump around if too many items are getting
-  // prepended at once. It also jumps around if items get prepended during scroll.
-  // To work around this, we prepend rows after scroll bumps against the top and rests.
-  const needsBumpMaxParents = React.useRef(false)
-  const onStartReached = React.useCallback(() => {
-    if (skeleton?.parents && maxParents < skeleton.parents.length) {
-      needsBumpMaxParents.current = true
-    }
-  }, [maxParents, skeleton?.parents])
-  const bumpMaxParentsIfNeeded = React.useCallback(() => {
-    if (!isNative) {
-      return
-    }
-    if (needsBumpMaxParents.current) {
-      needsBumpMaxParents.current = false
-      setMaxParents(n => n + PARENTS_CHUNK_SIZE)
-    }
-  }, [])
-  const onScrollToTop = bumpMaxParentsIfNeeded
-  const onMomentumEnd = React.useCallback(() => {
-    'worklet'
-    runOnJS(bumpMaxParentsIfNeeded)()
-  }, [bumpMaxParentsIfNeeded])
-
-  const onEndReached = React.useCallback(() => {
-    if (isFetching || posts.length < maxReplies) return
-    setMaxReplies(prev => prev + 50)
-  }, [isFetching, maxReplies, posts.length])
-
-  const onPostReply = React.useCallback(
-    (postUri: string | undefined) => {
-      refetch()
-      if (postUri) {
-        setJustPostedUris(set => {
-          const nextSet = new Set(set)
-          nextSet.add(postUri)
-          return nextSet
-        })
-      }
-    },
-    [refetch],
-  )
-
-  const {openComposer} = useOpenComposer()
-  const onReplyToAnchor = React.useCallback(() => {
-    if (thread?.type !== 'post') {
-      return
-    }
-    if (anchorPostSource) {
-      feedFeedback.sendInteraction({
-        item: thread.post.uri,
-        event: 'app.bsky.feed.defs#interactionReply',
-        feedContext: anchorPostSource.post.feedContext,
-        reqId: anchorPostSource.post.reqId,
-      })
-    }
-    openComposer({
-      replyTo: {
-        uri: thread.post.uri,
-        cid: thread.post.cid,
-        text: thread.record.text,
-        author: thread.post.author,
-        embed: thread.post.embed,
-        moderation: threadModerationCache.get(thread),
-        langs: thread.record.langs,
-      },
-      onPost: onPostReply,
-    })
-  }, [
-    openComposer,
-    thread,
-    onPostReply,
-    threadModerationCache,
-    anchorPostSource,
-    feedFeedback,
-  ])
-
-  const canReply = !error && rootPost && !rootPost.viewer?.replyDisabled
-  const hasParents =
-    skeleton?.highlightedPost?.type === 'post' &&
-    (skeleton.highlightedPost.ctx.isParentLoading ||
-      Boolean(skeleton?.parents && skeleton.parents.length > 0))
-
-  const renderItem = ({item, index}: {item: RowItem; index: number}) => {
-    if (item === REPLY_PROMPT && hasSession) {
-      return (
-        <View>
-          {!isMobile && (
-            <PostThreadComposePrompt onPressCompose={onReplyToAnchor} />
-          )}
-        </View>
-      )
-    } else if (item === SHOW_HIDDEN_REPLIES || item === SHOW_MUTED_REPLIES) {
-      return (
-        <PostThreadShowHiddenReplies
-          type={item === SHOW_HIDDEN_REPLIES ? 'hidden' : 'muted'}
-          onPress={() =>
-            setHiddenRepliesState(
-              item === SHOW_HIDDEN_REPLIES
-                ? HiddenRepliesState.Show
-                : HiddenRepliesState.ShowAndOverridePostHider,
-            )
-          }
-          hideTopBorder={index === 0}
-        />
-      )
-    } else if (isThreadNotFound(item)) {
-      return (
-        <View
-          style={[
-            a.p_lg,
-            index !== 0 && a.border_t,
-            t.atoms.border_contrast_low,
-            t.atoms.bg_contrast_25,
-          ]}>
-          <Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}>
-            <Trans>Deleted post.</Trans>
-          </Text>
-        </View>
-      )
-    } else if (isThreadBlocked(item)) {
-      return (
-        <View
-          style={[
-            a.p_lg,
-            index !== 0 && a.border_t,
-            t.atoms.border_contrast_low,
-            t.atoms.bg_contrast_25,
-          ]}>
-          <Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}>
-            <Trans>Blocked post.</Trans>
-          </Text>
-        </View>
-      )
-    } else if (isThreadPost(item)) {
-      const prev = isThreadPost(posts[index - 1])
-        ? (posts[index - 1] as ThreadPost)
-        : undefined
-      const next = isThreadPost(posts[index + 1])
-        ? (posts[index + 1] as ThreadPost)
-        : undefined
-      const showChildReplyLine = (next?.ctx.depth || 0) > item.ctx.depth
-      const showParentReplyLine =
-        (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1
-      const hasUnrevealedParents =
-        index === 0 && skeleton?.parents && maxParents < skeleton.parents.length
-
-      if (!treeView && prev && item.ctx.hasMoreSelfThread) {
-        return <PostThreadLoadMore post={prev.post} />
-      }
-
-      return (
-        <View
-          ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
-          onLayout={deferParents ? () => setDeferParents(false) : undefined}>
-          <PostThreadItem
-            post={item.post}
-            record={item.record}
-            threadgateRecord={threadgateRecord ?? undefined}
-            moderation={threadModerationCache.get(item)}
-            treeView={treeView}
-            depth={item.ctx.depth}
-            prevPost={prev}
-            nextPost={next}
-            isHighlightedPost={item.ctx.isHighlightedPost}
-            hasMore={item.ctx.hasMore}
-            showChildReplyLine={showChildReplyLine}
-            showParentReplyLine={showParentReplyLine}
-            hasPrecedingItem={showParentReplyLine || !!hasUnrevealedParents}
-            overrideBlur={
-              hiddenRepliesState ===
-                HiddenRepliesState.ShowAndOverridePostHider &&
-              item.ctx.depth > 0
-            }
-            onPostReply={onPostReply}
-            hideTopBorder={index === 0 && !item.ctx.isParentLoading}
-            anchorPostSource={anchorPostSource}
-          />
-        </View>
-      )
-    }
-    return null
-  }
-
-  if (!thread || !preferences || error) {
-    return (
-      <ListMaybePlaceholder
-        isLoading={!error}
-        isError={Boolean(error)}
-        noEmpty
-        onRetry={refetch}
-        errorTitle={error?.title}
-        errorMessage={error?.message}
-      />
-    )
-  }
-
-  return (
-    <>
-      <Header.Outer headerRef={headerRef}>
-        <Header.BackButton />
-        <Header.Content>
-          <Header.TitleText>
-            <Trans context="description">Post</Trans>
-          </Header.TitleText>
-        </Header.Content>
-        <Header.Slot>
-          <ThreadMenu
-            sortReplies={sortReplies}
-            treeViewEnabled={treeViewEnabled}
-            setSortReplies={updateSortReplies}
-            setTreeViewEnabled={updateTreeViewEnabled}
-          />
-        </Header.Slot>
-      </Header.Outer>
-
-      <ScrollProvider onMomentumEnd={onMomentumEnd}>
-        <List
-          ref={ref}
-          data={posts}
-          renderItem={renderItem}
-          keyExtractor={keyExtractor}
-          onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
-          onStartReached={onStartReached}
-          onEndReached={onEndReached}
-          onEndReachedThreshold={2}
-          onScrollToTop={onScrollToTop}
-          /**
-           * @see https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition
-           */
-          maintainVisibleContentPosition={
-            isNative && hasParents
-              ? MAINTAIN_VISIBLE_CONTENT_POSITION
-              : undefined
-          }
-          desktopFixedHeight
-          removeClippedSubviews={isAndroid ? false : undefined}
-          ListFooterComponent={
-            <ListFooter
-              // Using `isFetching` over `isFetchingNextPage` is done on purpose here so we get the loader on
-              // initial render
-              isFetchingNextPage={isFetching}
-              error={cleanError(threadError)}
-              onRetry={refetch}
-              // 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to
-              // work without causing weird jumps on web or glitches on native
-              height={windowHeight - 200}
-            />
-          }
-          initialNumToRender={initialNumToRender}
-          windowSize={11}
-          sideBorders={false}
-        />
-      </ScrollProvider>
-      {isMobile && canReply && hasSession && (
-        <MobileComposePrompt onPressReply={onReplyToAnchor} />
-      )}
-    </>
-  )
-}
-
-let ThreadMenu = ({
-  sortReplies,
-  treeViewEnabled,
-  setSortReplies,
-  setTreeViewEnabled,
-}: {
-  sortReplies: string
-  treeViewEnabled: boolean
-  setSortReplies: (newValue: string) => void
-  setTreeViewEnabled: (newValue: boolean) => void
-}): React.ReactNode => {
-  const {_} = useLingui()
-  return (
-    <Menu.Root>
-      <Menu.Trigger label={_(msg`Thread options`)}>
-        {({props}) => (
-          <Button
-            label={_(msg`Thread options`)}
-            size="small"
-            variant="ghost"
-            color="secondary"
-            shape="round"
-            hitSlop={HITSLOP_10}
-            {...props}>
-            <ButtonIcon icon={SettingsSlider} size="md" />
-          </Button>
-        )}
-      </Menu.Trigger>
-      <Menu.Outer>
-        <Menu.LabelText>
-          <Trans>Show replies as</Trans>
-        </Menu.LabelText>
-        <Menu.Group>
-          <Menu.Item
-            label={_(msg`Linear`)}
-            onPress={() => {
-              setTreeViewEnabled(false)
-            }}>
-            <Menu.ItemText>
-              <Trans>Linear</Trans>
-            </Menu.ItemText>
-            <Menu.ItemRadio selected={!treeViewEnabled} />
-          </Menu.Item>
-          <Menu.Item
-            label={_(msg`Threaded`)}
-            onPress={() => {
-              setTreeViewEnabled(true)
-            }}>
-            <Menu.ItemText>
-              <Trans>Threaded</Trans>
-            </Menu.ItemText>
-            <Menu.ItemRadio selected={treeViewEnabled} />
-          </Menu.Item>
-        </Menu.Group>
-        <Menu.Divider />
-        <Menu.LabelText>
-          <Trans>Reply sorting</Trans>
-        </Menu.LabelText>
-        <Menu.Group>
-          <Menu.Item
-            label={_(msg`Hot replies first`)}
-            onPress={() => {
-              setSortReplies('hotness')
-            }}>
-            <Menu.ItemText>
-              <Trans>Hot replies first</Trans>
-            </Menu.ItemText>
-            <Menu.ItemRadio selected={sortReplies === 'hotness'} />
-          </Menu.Item>
-          <Menu.Item
-            label={_(msg`Oldest replies first`)}
-            onPress={() => {
-              setSortReplies('oldest')
-            }}>
-            <Menu.ItemText>
-              <Trans>Oldest replies first</Trans>
-            </Menu.ItemText>
-            <Menu.ItemRadio selected={sortReplies === 'oldest'} />
-          </Menu.Item>
-          <Menu.Item
-            label={_(msg`Newest replies first`)}
-            onPress={() => {
-              setSortReplies('newest')
-            }}>
-            <Menu.ItemText>
-              <Trans>Newest replies first</Trans>
-            </Menu.ItemText>
-            <Menu.ItemRadio selected={sortReplies === 'newest'} />
-          </Menu.Item>
-          <Menu.Item
-            label={_(msg`Most-liked replies first`)}
-            onPress={() => {
-              setSortReplies('most-likes')
-            }}>
-            <Menu.ItemText>
-              <Trans>Most-liked replies first</Trans>
-            </Menu.ItemText>
-            <Menu.ItemRadio selected={sortReplies === 'most-likes'} />
-          </Menu.Item>
-          <Menu.Item
-            label={_(msg`Random (aka "Poster's Roulette")`)}
-            onPress={() => {
-              setSortReplies('random')
-            }}>
-            <Menu.ItemText>
-              <Trans>Random (aka "Poster's Roulette")</Trans>
-            </Menu.ItemText>
-            <Menu.ItemRadio selected={sortReplies === 'random'} />
-          </Menu.Item>
-        </Menu.Group>
-      </Menu.Outer>
-    </Menu.Root>
-  )
-}
-ThreadMenu = memo(ThreadMenu)
-
-function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
-  const {footerHeight} = useShellLayout()
-
-  const animatedStyle = useAnimatedStyle(() => {
-    return {
-      bottom: footerHeight.get(),
-    }
-  })
-
-  return (
-    <Animated.View style={[a.fixed, a.left_0, a.right_0, animatedStyle]}>
-      <PostThreadComposePrompt onPressCompose={onPressReply} />
-    </Animated.View>
-  )
-}
-
-function isThreadPost(v: unknown): v is ThreadPost {
-  return !!v && typeof v === 'object' && 'type' in v && v.type === 'post'
-}
-
-function isThreadNotFound(v: unknown): v is ThreadNotFound {
-  return !!v && typeof v === 'object' && 'type' in v && v.type === 'not-found'
-}
-
-function isThreadBlocked(v: unknown): v is ThreadBlocked {
-  return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked'
-}
-
-function createThreadSkeleton(
-  node: ThreadNode,
-  currentDid: string | undefined,
-  treeView: boolean,
-  modCache: ThreadModerationCache,
-  showHiddenReplies: boolean,
-  threadgateRecordHiddenReplies: Set<string>,
-): ThreadSkeletonParts | null {
-  if (!node) return null
-
-  return {
-    parents: Array.from(flattenThreadParents(node, !!currentDid)),
-    highlightedPost: node,
-    replies: Array.from(
-      flattenThreadReplies(
-        node,
-        currentDid,
-        treeView,
-        modCache,
-        showHiddenReplies,
-        threadgateRecordHiddenReplies,
-      ),
-    ),
-  }
-}
-
-function* flattenThreadParents(
-  node: ThreadNode,
-  hasSession: boolean,
-): Generator<YieldedItem, void> {
-  if (node.type === 'post') {
-    if (node.parent) {
-      yield* flattenThreadParents(node.parent, hasSession)
-    }
-    if (!node.ctx.isHighlightedPost) {
-      yield node
-    }
-  } else if (node.type === 'not-found') {
-    yield node
-  } else if (node.type === 'blocked') {
-    yield node
-  }
-}
-
-// The enum is ordered to make them easy to merge
-enum HiddenReplyType {
-  None = 0,
-  Muted = 1,
-  Hidden = 2,
-}
-
-function* flattenThreadReplies(
-  node: ThreadNode,
-  currentDid: string | undefined,
-  treeView: boolean,
-  modCache: ThreadModerationCache,
-  showHiddenReplies: boolean,
-  threadgateRecordHiddenReplies: Set<string>,
-): Generator<YieldedItem, HiddenReplyType> {
-  if (node.type === 'post') {
-    // dont show pwi-opted-out posts to logged out users
-    if (!currentDid && hasPwiOptOut(node)) {
-      return HiddenReplyType.None
-    }
-
-    // handle blurred items
-    if (node.ctx.depth > 0) {
-      const modui = modCache.get(node)?.ui('contentList')
-      if (modui?.blur || modui?.filter) {
-        if (!showHiddenReplies || node.ctx.depth > 1) {
-          if ((modui.blurs[0] || modui.filters[0]).type === 'muted') {
-            return HiddenReplyType.Muted
-          }
-          return HiddenReplyType.Hidden
-        }
-      }
-
-      if (!showHiddenReplies) {
-        const hiddenByThreadgate = threadgateRecordHiddenReplies.has(
-          node.post.uri,
-        )
-        const authorIsViewer = node.post.author.did === currentDid
-        if (hiddenByThreadgate && !authorIsViewer) {
-          return HiddenReplyType.Hidden
-        }
-      }
-    }
-
-    if (!node.ctx.isHighlightedPost) {
-      yield node
-    }
-
-    if (node.replies?.length) {
-      let hiddenReplies = HiddenReplyType.None
-      for (const reply of node.replies) {
-        let hiddenReply = yield* flattenThreadReplies(
-          reply,
-          currentDid,
-          treeView,
-          modCache,
-          showHiddenReplies,
-          threadgateRecordHiddenReplies,
-        )
-        if (hiddenReply > hiddenReplies) {
-          hiddenReplies = hiddenReply
-        }
-        if (!treeView && !node.ctx.isHighlightedPost) {
-          break
-        }
-      }
-
-      // show control to enable hidden replies
-      if (node.ctx.depth === 0) {
-        if (hiddenReplies === HiddenReplyType.Muted) {
-          yield SHOW_MUTED_REPLIES
-        } else if (hiddenReplies === HiddenReplyType.Hidden) {
-          yield SHOW_HIDDEN_REPLIES
-        }
-      }
-    }
-  } else if (node.type === 'not-found') {
-    yield node
-  } else if (node.type === 'blocked') {
-    yield node
-  }
-  return HiddenReplyType.None
-}
-
-function hasPwiOptOut(node: ThreadPost) {
-  return !!node.post.author.labels?.find(l => l.val === '!no-unauthenticated')
-}
-
-function hasBranchingReplies(node?: ThreadNode) {
-  if (!node) {
-    return false
-  }
-  if (node.type !== 'post') {
-    return false
-  }
-  if (!node.replies) {
-    return false
-  }
-  if (node.replies.length === 1) {
-    return hasBranchingReplies(node.replies[0])
-  }
-  return true
-}
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
deleted file mode 100644
index 679a506b9..000000000
--- 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 <PostThreadItemDeleted hideTopBorder={hideTopBorder} />
-  }
-  if (richText && moderation) {
-    return (
-      <PostThreadItemLoaded
-        // Safeguard from clobbering per-post state below:
-        key={postShadowed.uri}
-        post={postShadowed}
-        prevPost={prevPost}
-        nextPost={nextPost}
-        record={record}
-        richText={richText}
-        moderation={moderation}
-        treeView={treeView}
-        depth={depth}
-        isHighlightedPost={isHighlightedPost}
-        hasMore={hasMore}
-        showChildReplyLine={showChildReplyLine}
-        showParentReplyLine={showParentReplyLine}
-        hasPrecedingItem={hasPrecedingItem}
-        overrideBlur={overrideBlur}
-        onPostReply={onPostReply}
-        onPostSuccess={onPostSuccess}
-        hideTopBorder={hideTopBorder}
-        threadgateRecord={threadgateRecord}
-        anchorPostSource={anchorPostSource}
-      />
-    )
-  }
-  return null
-}
-
-function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
-  const t = useTheme()
-  return (
-    <View
-      style={[
-        t.atoms.bg,
-        t.atoms.border_contrast_low,
-        a.p_xl,
-        a.pl_lg,
-        a.flex_row,
-        a.gap_md,
-        !hideTopBorder && a.border_t,
-      ]}>
-      <TrashIcon style={[t.atoms.text]} />
-      <Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}>
-        <Trans>This post has been deleted.</Trans>
-      </Text>
-    </View>
-  )
-}
-
-let PostThreadItemLoaded = ({
-  post,
-  record,
-  richText,
-  moderation,
-  treeView,
-  depth,
-  prevPost,
-  nextPost,
-  isHighlightedPost,
-  hasMore,
-  showChildReplyLine,
-  showParentReplyLine,
-  hasPrecedingItem,
-  overrideBlur,
-  onPostReply,
-  onPostSuccess,
-  hideTopBorder,
-  threadgateRecord,
-  anchorPostSource,
-}: {
-  post: Shadow<AppBskyFeedDefs.PostView>
-  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 <ErrorMessage message={_(msg`Invalid or unsupported post record`)} />
-  }
-
-  if (isHighlightedPost) {
-    return (
-      <>
-        {rootUri !== post.uri && (
-          <View
-            style={[
-              a.pl_lg,
-              a.flex_row,
-              a.pb_xs,
-              {height: a.pt_lg.paddingTop},
-            ]}>
-            <View style={{width: 42}}>
-              <View
-                style={[
-                  styles.replyLine,
-                  a.flex_grow,
-                  {backgroundColor: pal.colors.replyLine},
-                ]}
-              />
-            </View>
-          </View>
-        )}
-
-        <View
-          testID={`postThreadItem-by-${post.author.handle}`}
-          style={[
-            a.px_lg,
-            t.atoms.border_contrast_low,
-            // root post styles
-            rootUri === post.uri && [a.pt_lg],
-          ]}>
-          <View style={[a.flex_row, a.gap_md, a.pb_md]}>
-            <PreviewableUserAvatar
-              size={42}
-              profile={post.author}
-              moderation={moderation.ui('avatar')}
-              type={post.author.associated?.labeler ? 'labeler' : 'user'}
-              live={live}
-              onBeforePress={onOpenAuthor}
-            />
-            <View style={[a.flex_1]}>
-              <View style={[a.flex_row, a.align_center]}>
-                <Link
-                  style={[a.flex_shrink]}
-                  href={authorHref}
-                  title={authorTitle}
-                  onBeforePress={onOpenAuthor}>
-                  <Text
-                    emoji
-                    style={[
-                      a.text_lg,
-                      a.font_bold,
-                      a.leading_snug,
-                      a.self_start,
-                    ]}
-                    numberOfLines={1}>
-                    {sanitizeDisplayName(
-                      post.author.displayName ||
-                        sanitizeHandle(post.author.handle),
-                      moderation.ui('displayName'),
-                    )}
-                  </Text>
-                </Link>
-
-                <View style={[{paddingLeft: 3, top: -1}]}>
-                  <VerificationCheckButton
-                    profile={shadowedPostAuthor}
-                    size="md"
-                  />
-                </View>
-              </View>
-              <Link style={s.flex1} href={authorHref} title={authorTitle}>
-                <Text
-                  emoji
-                  style={[
-                    a.text_md,
-                    a.leading_snug,
-                    t.atoms.text_contrast_medium,
-                  ]}
-                  numberOfLines={1}>
-                  {sanitizeHandle(post.author.handle, '@')}
-                </Text>
-              </Link>
-            </View>
-            {showFollowButton && (
-              <View>
-                <PostThreadFollowBtn did={post.author.did} />
-              </View>
-            )}
-          </View>
-          <View style={[a.pb_sm]}>
-            <LabelsOnMyPost post={post} style={[a.pb_sm]} />
-            <ContentHider
-              modui={moderation.ui('contentView')}
-              ignoreMute
-              childContainerStyle={[a.pt_sm]}>
-              <PostAlerts
-                modui={moderation.ui('contentView')}
-                size="lg"
-                includeMute
-                style={[a.pb_sm]}
-                additionalCauses={additionalPostAlerts}
-              />
-              {richText?.text ? (
-                <RichText
-                  enableTags
-                  selectable
-                  value={richText}
-                  style={[a.flex_1, a.text_xl]}
-                  authorHandle={post.author.handle}
-                  shouldProxyLinks={true}
-                />
-              ) : undefined}
-              {post.embed && (
-                <View style={[a.py_xs]}>
-                  <Embed
-                    embed={post.embed}
-                    moderation={moderation}
-                    viewContext={PostEmbedViewContext.ThreadHighlighted}
-                    onOpen={onOpenEmbed}
-                  />
-                </View>
-              )}
-            </ContentHider>
-            <ExpandedPostDetails
-              post={post}
-              record={record}
-              isThreadAuthor={isThreadAuthor}
-              needsTranslation={needsTranslation}
-            />
-            {post.repostCount !== 0 ||
-            post.likeCount !== 0 ||
-            post.quoteCount !== 0 ? (
-              // Show this section unless we're *sure* it has no engagement.
-              <View
-                style={[
-                  a.flex_row,
-                  a.align_center,
-                  a.gap_lg,
-                  a.border_t,
-                  a.border_b,
-                  a.mt_md,
-                  a.py_md,
-                  t.atoms.border_contrast_low,
-                ]}>
-                {post.repostCount != null && post.repostCount !== 0 ? (
-                  <Link href={repostsHref} title={repostsTitle}>
-                    <Text
-                      testID="repostCount-expanded"
-                      style={[a.text_md, t.atoms.text_contrast_medium]}>
-                      <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                        {formatCount(i18n, post.repostCount)}
-                      </Text>{' '}
-                      <Plural
-                        value={post.repostCount}
-                        one="repost"
-                        other="reposts"
-                      />
-                    </Text>
-                  </Link>
-                ) : null}
-                {post.quoteCount != null &&
-                post.quoteCount !== 0 &&
-                !post.viewer?.embeddingDisabled ? (
-                  <Link href={quotesHref} title={quotesTitle}>
-                    <Text
-                      testID="quoteCount-expanded"
-                      style={[a.text_md, t.atoms.text_contrast_medium]}>
-                      <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                        {formatCount(i18n, post.quoteCount)}
-                      </Text>{' '}
-                      <Plural
-                        value={post.quoteCount}
-                        one="quote"
-                        other="quotes"
-                      />
-                    </Text>
-                  </Link>
-                ) : null}
-                {post.likeCount != null && post.likeCount !== 0 ? (
-                  <Link href={likesHref} title={likesTitle}>
-                    <Text
-                      testID="likeCount-expanded"
-                      style={[a.text_md, t.atoms.text_contrast_medium]}>
-                      <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
-                        {formatCount(i18n, post.likeCount)}
-                      </Text>{' '}
-                      <Plural value={post.likeCount} one="like" other="likes" />
-                    </Text>
-                  </Link>
-                ) : null}
-              </View>
-            ) : null}
-            <View
-              style={[
-                a.pt_sm,
-                a.pb_2xs,
-                {
-                  marginLeft: -5,
-                },
-              ]}>
-              <FeedFeedbackProvider value={feedFeedback}>
-                <PostControls
-                  big
-                  post={post}
-                  record={record}
-                  richText={richText}
-                  onPressReply={onPressReply}
-                  onPostReply={onPostReply}
-                  logContext="PostThreadItem"
-                  threadgateRecord={threadgateRecord}
-                  feedContext={anchorPostSource?.post?.feedContext}
-                  reqId={anchorPostSource?.post?.reqId}
-                  viaRepost={viaRepost}
-                />
-              </FeedFeedbackProvider>
-            </View>
-          </View>
-        </View>
-      </>
-    )
-  } else {
-    const isThreadedChild = treeView && depth > 0
-    const isThreadedChildAdjacentTop =
-      isThreadedChild && prevPost?.ctx.depth === depth && depth !== 1
-    const isThreadedChildAdjacentBot =
-      isThreadedChild && nextPost?.ctx.depth === depth
-    return (
-      <PostOuterWrapper
-        post={post}
-        depth={depth}
-        showParentReplyLine={!!showParentReplyLine}
-        treeView={treeView}
-        hasPrecedingItem={hasPrecedingItem}
-        hideTopBorder={hideTopBorder}>
-        <PostHider
-          testID={`postThreadItem-by-${post.author.handle}`}
-          href={postHref}
-          disabled={overrideBlur}
-          modui={moderation.ui('contentList')}
-          iconSize={isThreadedChild ? 24 : 42}
-          iconStyles={
-            isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2}
-          }
-          profile={post.author}
-          interpretFilterAsBlur>
-          <View
-            style={{
-              flexDirection: 'row',
-              gap: 10,
-              paddingLeft: 8,
-              height: isThreadedChildAdjacentTop ? 8 : 16,
-            }}>
-            <View style={{width: 42}}>
-              {!isThreadedChild && showParentReplyLine && (
-                <View
-                  style={[
-                    styles.replyLine,
-                    {
-                      flexGrow: 1,
-                      backgroundColor: pal.colors.replyLine,
-                      marginBottom: 4,
-                    },
-                  ]}
-                />
-              )}
-            </View>
-          </View>
-
-          <View
-            style={[
-              a.flex_row,
-              a.px_sm,
-              a.gap_md,
-              {
-                paddingBottom:
-                  showChildReplyLine && !isThreadedChild
-                    ? 0
-                    : isThreadedChildAdjacentBot
-                      ? 4
-                      : 8,
-              },
-            ]}>
-            {/* If we are in threaded mode, the avatar is rendered in PostMeta */}
-            {!isThreadedChild && (
-              <View>
-                <PreviewableUserAvatar
-                  size={42}
-                  profile={post.author}
-                  moderation={moderation.ui('avatar')}
-                  type={post.author.associated?.labeler ? 'labeler' : 'user'}
-                  live={live}
-                />
-
-                {showChildReplyLine && (
-                  <View
-                    style={[
-                      styles.replyLine,
-                      {
-                        flexGrow: 1,
-                        backgroundColor: pal.colors.replyLine,
-                        marginTop: 4,
-                      },
-                    ]}
-                  />
-                )}
-              </View>
-            )}
-
-            <View style={[a.flex_1]}>
-              <PostMeta
-                author={post.author}
-                moderation={moderation}
-                timestamp={post.indexedAt}
-                postHref={postHref}
-                showAvatar={isThreadedChild}
-                avatarSize={24}
-                style={[a.pb_xs]}
-              />
-              <LabelsOnMyPost post={post} style={[a.pb_xs]} />
-              <PostAlerts
-                modui={moderation.ui('contentList')}
-                style={[a.pb_2xs]}
-                additionalCauses={additionalPostAlerts}
-              />
-              {richText?.text ? (
-                <View style={[a.pb_2xs, a.pr_sm]}>
-                  <RichText
-                    enableTags
-                    value={richText}
-                    style={[a.flex_1, a.text_md]}
-                    numberOfLines={limitLines ? MAX_POST_LINES : undefined}
-                    authorHandle={post.author.handle}
-                    shouldProxyLinks={true}
-                  />
-                  {limitLines && (
-                    <ShowMoreTextButton
-                      style={[a.text_md]}
-                      onPress={onPressShowMore}
-                    />
-                  )}
-                </View>
-              ) : undefined}
-              {post.embed && (
-                <View style={[a.pb_xs]}>
-                  <Embed
-                    embed={post.embed}
-                    moderation={moderation}
-                    viewContext={PostEmbedViewContext.Feed}
-                  />
-                </View>
-              )}
-              <PostControls
-                post={post}
-                record={record}
-                richText={richText}
-                onPressReply={onPressReply}
-                logContext="PostThreadItem"
-                threadgateRecord={threadgateRecord}
-              />
-            </View>
-          </View>
-          {hasMore ? (
-            <Link
-              style={[
-                styles.loadMore,
-                {
-                  paddingLeft: treeView ? 8 : 70,
-                  paddingTop: 0,
-                  paddingBottom: treeView ? 4 : 12,
-                },
-              ]}
-              href={postHref}
-              title={itemTitle}
-              noFeedback>
-              <Text
-                style={[t.atoms.text_contrast_medium, a.font_bold, a.text_sm]}>
-                <Trans>More</Trans>
-              </Text>
-              <ChevronRightIcon
-                size="xs"
-                style={[t.atoms.text_contrast_medium]}
-              />
-            </Link>
-          ) : undefined}
-        </PostHider>
-      </PostOuterWrapper>
-    )
-  }
-}
-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 (
-      <View
-        style={[
-          a.flex_row,
-          a.px_sm,
-          a.flex_row,
-          t.atoms.border_contrast_low,
-          styles.cursor,
-          depth === 1 && a.border_t,
-        ]}
-        onPointerEnter={onHoverIn}
-        onPointerLeave={onHoverOut}>
-        {Array.from(Array(depth - 1)).map((_, n: number) => (
-          <View
-            key={`${post.uri}-padding-${n}`}
-            style={[
-              a.ml_sm,
-              t.atoms.border_contrast_low,
-              {
-                borderLeftWidth: 2,
-                paddingLeft: a.pl_sm.paddingLeft - 2, // minus border
-              },
-            ]}
-          />
-        ))}
-        <View style={a.flex_1}>
-          <SubtleWebHover
-            hover={hover}
-            style={{
-              left: (depth === 1 ? 0 : 2) - a.pl_sm.paddingLeft,
-              right: -a.pr_sm.paddingRight,
-            }}
-          />
-          {children}
-        </View>
-      </View>
-    )
-  }
-  return (
-    <View
-      onPointerEnter={onHoverIn}
-      onPointerLeave={onHoverOut}
-      style={[
-        a.border_t,
-        a.px_sm,
-        t.atoms.border_contrast_low,
-        showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
-        hideTopBorder && styles.noTopBorder,
-        styles.cursor,
-      ]}>
-      <SubtleWebHover hover={hover} />
-      {children}
-    </View>
-  )
-}
-
-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<AppBskyFeedPost.Record>(
-          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 (
-    <View style={[a.gap_md, a.pt_md, a.align_start]}>
-      <BackdatedPostIndicator post={post} />
-      <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
-        <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
-          {niceDate(i18n, post.indexedAt)}
-        </Text>
-        {isRootPost && (
-          <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
-        )}
-        {needsTranslation && (
-          <>
-            <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
-              &middot;
-            </Text>
-
-            <InlineLinkText
-              // overridden to open an intent on android, but keep
-              // as anchor tag for accessibility
-              to={getTranslatorLink(record.text, langPrefs.primaryLanguage)}
-              label={_(msg`Translate`)}
-              style={[a.text_sm, pal.link]}
-              onPress={onTranslatePress}>
-              <Trans>Translate</Trans>
-            </InlineLinkText>
-          </>
-        )}
-      </View>
-    </View>
-  )
-}
-
-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<AppBskyFeedPost.Record>(
-    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 (
-    <>
-      <Button
-        label={_(msg`Archived post`)}
-        accessibilityHint={_(
-          msg`Shows information about when this post was created`,
-        )}
-        onPress={e => {
-          e.preventDefault()
-          e.stopPropagation()
-          control.open()
-        }}>
-        {({hovered, pressed}) => (
-          <View
-            style={[
-              a.flex_row,
-              a.align_center,
-              a.rounded_full,
-              t.atoms.bg_contrast_25,
-              (hovered || pressed) && t.atoms.bg_contrast_50,
-              {
-                gap: 3,
-                paddingHorizontal: 6,
-                paddingVertical: 3,
-              },
-            ]}>
-            <CalendarClockIcon fill={orange} size="sm" aria-hidden />
-            <Text
-              style={[
-                a.text_xs,
-                a.font_bold,
-                a.leading_tight,
-                t.atoms.text_contrast_medium,
-              ]}>
-              <Trans>Archived from {niceDate(i18n, createdAt)}</Trans>
-            </Text>
-          </View>
-        )}
-      </Button>
-
-      <Prompt.Outer control={control}>
-        <Prompt.TitleText>
-          <Trans>Archived post</Trans>
-        </Prompt.TitleText>
-        <Prompt.DescriptionText>
-          <Trans>
-            This post claims to have been created on{' '}
-            <RNText style={[a.font_bold]}>{niceDate(i18n, createdAt)}</RNText>,
-            but was first seen by Bluesky on{' '}
-            <RNText style={[a.font_bold]}>{niceDate(i18n, indexedAt)}</RNText>.
-          </Trans>
-        </Prompt.DescriptionText>
-        <Text
-          style={[
-            a.text_md,
-            a.leading_snug,
-            t.atoms.text_contrast_high,
-            a.pb_xl,
-          ]}>
-          <Trans>
-            Bluesky cannot confirm the authenticity of the claimed date.
-          </Trans>
-        </Text>
-        <Prompt.Actions>
-          <Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} />
-        </Prompt.Actions>
-      </Prompt.Outer>
-    </>
-  )
-}
-
-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/com/post-thread/PostThreadLoadMore.tsx b/src/view/com/post-thread/PostThreadLoadMore.tsx
deleted file mode 100644
index 27e2ea724..000000000
--- a/src/view/com/post-thread/PostThreadLoadMore.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import * as React from 'react'
-import {View} from 'react-native'
-import {AppBskyFeedDefs, AtUri} from '@atproto/api'
-import {Trans} from '@lingui/macro'
-
-import {makeProfileLink} from '#/lib/routes/links'
-import {atoms as a, useTheme} from '#/alf'
-import {Text} from '#/components/Typography'
-import {Link} from '../util/Link'
-import {UserAvatar} from '../util/UserAvatar'
-
-export function PostThreadLoadMore({post}: {post: AppBskyFeedDefs.PostView}) {
-  const t = useTheme()
-
-  const postHref = React.useMemo(() => {
-    const urip = new AtUri(post.uri)
-    return makeProfileLink(post.author, 'post', urip.rkey)
-  }, [post.uri, post.author])
-
-  return (
-    <Link
-      href={postHref}
-      style={[a.flex_row, a.align_center, a.py_md, {paddingHorizontal: 14}]}
-      hoverStyle={[t.atoms.bg_contrast_25]}>
-      <View style={[a.flex_row]}>
-        <View
-          style={{
-            alignItems: 'center',
-            justifyContent: 'center',
-            width: 34,
-            height: 34,
-            borderRadius: 18,
-            backgroundColor: t.atoms.bg.backgroundColor,
-            marginRight: -20,
-          }}>
-          <UserAvatar
-            avatar={post.author.avatar}
-            size={30}
-            type={post.author.associated?.labeler ? 'labeler' : 'user'}
-          />
-        </View>
-        <View
-          style={{
-            alignItems: 'center',
-            justifyContent: 'center',
-            width: 34,
-            height: 34,
-            borderRadius: 18,
-            backgroundColor: t.atoms.bg.backgroundColor,
-          }}>
-          <UserAvatar
-            avatar={post.author.avatar}
-            size={30}
-            type={post.author.associated?.labeler ? 'labeler' : 'user'}
-          />
-        </View>
-      </View>
-      <View style={[a.px_sm]}>
-        <Text style={[{color: t.palette.primary_500}, a.text_md]}>
-          <Trans>Continue thread...</Trans>
-        </Text>
-      </View>
-    </Link>
-  )
-}
diff --git a/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx b/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
deleted file mode 100644
index 7dc75520b..000000000
--- a/src/view/com/post-thread/PostThreadShowHiddenReplies.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import {View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
-import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
-import {Text} from '#/components/Typography'
-
-export function PostThreadShowHiddenReplies({
-  type,
-  onPress,
-  hideTopBorder,
-}: {
-  type: 'hidden' | 'muted'
-  onPress: () => void
-  hideTopBorder?: boolean
-}) {
-  const {_} = useLingui()
-  const t = useTheme()
-  const label =
-    type === 'muted' ? _(msg`Show muted replies`) : _(msg`Show hidden replies`)
-
-  return (
-    <Button onPress={onPress} label={label}>
-      {({hovered, pressed}) => (
-        <View
-          style={[
-            a.flex_1,
-            a.flex_row,
-            a.align_center,
-            a.gap_sm,
-            a.py_lg,
-            a.px_xl,
-            !hideTopBorder && a.border_t,
-            t.atoms.border_contrast_low,
-            hovered || pressed ? t.atoms.bg_contrast_25 : t.atoms.bg,
-          ]}>
-          <View
-            style={[
-              t.atoms.bg_contrast_25,
-              a.align_center,
-              a.justify_center,
-              {
-                width: 26,
-                height: 26,
-                borderRadius: 13,
-                marginRight: 4,
-              },
-            ]}>
-            <EyeSlash size="sm" fill={t.atoms.text_contrast_medium.color} />
-          </View>
-          <Text
-            style={[t.atoms.text_contrast_medium, a.flex_1, a.leading_snug]}
-            numberOfLines={1}>
-            {label}
-          </Text>
-        </View>
-      )}
-    </Button>
-  )
-}
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
index d2e492f7e..f2afe8235 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<
                   <MockPostFeedItem post={post} moderation={postModeration} />
 
                   <Heading title="Post" subtitle="viewed directly" />
-                  <MockPostThreadItem post={post} moderation={postModeration} />
+                  <MockPostThreadItem post={post} moderationOpts={modOpts} />
 
                   <Heading title="Post" subtitle="reply in thread" />
                   <MockPostThreadItem
                     post={post}
-                    moderation={postModeration}
-                    reply
+                    moderationOpts={modOpts}
+                    isReply
                   />
                 </>
               )}
@@ -837,28 +839,33 @@ function MockPostFeedItem({
 
 function MockPostThreadItem({
   post,
-  moderation,
-  reply,
+  moderationOpts,
+  isReply,
 }: {
   post: AppBskyFeedDefs.PostView
-  moderation: ModerationDecision
-  reply?: boolean
+  moderationOpts: ModerationOpts
+  isReply?: boolean
 }) {
-  return (
-    <PostThreadItem
-      // @ts-ignore
-      post={post}
-      record={post.record as AppBskyFeedPost.Record}
-      moderation={moderation}
-      depth={reply ? 1 : 0}
-      isHighlightedPost={!reply}
-      treeView={false}
-      prevPost={undefined}
-      nextPost={undefined}
-      hasPrecedingItem={false}
-      overrideBlur={false}
-      onPostReply={() => {}}
-    />
+  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<string>(),
+  })
+
+  return isReply ? (
+    <ThreadItemPost item={thread} />
+  ) : (
+    <ThreadItemAnchor item={thread} />
   )
 }
 
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index cc611e0d6..f07c971fb 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -5,17 +5,14 @@ import {
   type CommonNavigatorParams,
   type NativeStackScreenProps,
 } from '#/lib/routes/types'
-import {useGate} from '#/lib/statsig/statsig'
 import {makeRecordUri} from '#/lib/strings/url-helpers'
 import {useSetMinimalShellMode} from '#/state/shell'
-import {PostThread as PostThreadComponent} from '#/view/com/post-thread/PostThread'
 import {PostThread} from '#/screens/PostThread'
 import * as Layout from '#/components/Layout'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
 export function PostThreadScreen({route}: Props) {
   const setMinimalShellMode = useSetMinimalShellMode()
-  const gate = useGate()
 
   const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
@@ -28,11 +25,7 @@ export function PostThreadScreen({route}: Props) {
 
   return (
     <Layout.Screen testID="postThreadScreen">
-      {gate('post_threads_v2_unspecced') || __DEV__ ? (
-        <PostThread uri={uri} />
-      ) : (
-        <PostThreadComponent uri={uri} />
-      )}
+      <PostThread uri={uri} />
     </Layout.Screen>
   )
 }