about summary refs log tree commit diff
path: root/src/view/com/post-thread/PostThread.tsx
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 /src/view/com/post-thread/PostThread.tsx
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>
Diffstat (limited to 'src/view/com/post-thread/PostThread.tsx')
-rw-r--r--src/view/com/post-thread/PostThread.tsx910
1 files changed, 0 insertions, 910 deletions
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
-}