about summary refs log tree commit diff
path: root/src/state/queries/usePostThread/index.ts
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-06-11 14:32:14 -0500
committerGitHub <noreply@github.com>2025-06-11 14:32:14 -0500
commit61004b887b0c7515837e051144b694fc7db5a1cc (patch)
tree08cda716a97867480996f21d384824987fe3c15b /src/state/queries/usePostThread/index.ts
parent143d5f3b814f1ce707fdfc87dabff7af5349bd06 (diff)
downloadvoidsky-61004b887b0c7515837e051144b694fc7db5a1cc.tar.zst
[Threads V2] Preliminary integration of unspecced V2 APIs (#8443)
* WIP

* Sorting working

* Rough handling of hidden/muted

* Better muted/hidden sorting and handling

* Clarify some naming

* Fix parents

* Handle first reply under highlighted/composer

* WIP RaW

* WIP optimistic

* Optimistic WIP

* Little cleanup, inserting dupes

* Re-org

* Add in new optimistic insert logic

* Update types

* Sorta working linear view optimistic state

* Simple working version, no pref for OP

* Working optimistic reply insertions, preference for OP

* Ensure deletes are coming through

* WIP scroll handling

* WIP scroll tweaks

* Clean up scrolling

* Clean up onPostSuccess

* Add annotations

* Fix highlighted post calc

* WIP kill me

* Update APIs

* Nvm don't kill me

* Fix optimistic insert

* Handle read more cases in tree view

* Basically working read more

* Handle linear view

* Reorg

* More reorg

* Split up thread post components

* New reply tree layout

* Fix up traversal metadata

* Tighten some spacing

* Use indent ya idiot

* Some linear mode cleanup

* Fix lines on read more items

* Vibe coding to success

* Almost there with read mores

* Update APIs

* Bump sdk

* Update import

* Checkpoint new traversal

* Checkpoint cleanup

* Checkpoint, need to fix blocked posts

* Checkpoint: think we're good, needs more cleanup

* Clean it up

* Two passes only

* Set to default params, update comment

* Fix render bug on native

* Checkpoint parent rendering, can opt for slower handling here

* Clean up parent handling, reply handling

* Fix read more extra space

* Fix read more in linear view

* Fix hidden reply handling, seen count, before/after calc

* Update naming

* Rename Slice to ThreadItem

* Add basic post and anchor skeletons

* Refactor client-side hidden

* WIP hidden fetching

* Update types

* Clean up query a bit

* Scrolling still broken

* Ok maybe fix scrolling

* Checkpoint move state into meta query

* Don't load remote hidden items unless needed

* skeleton view

* Reset hidden items when params change

* Split up traversal and avoid multiple passes

* Clean up

* Checkpoint: handling exhausted replies

* Clean up traversal functions further

* Clean up pagination

* Limit optimistic reply depth

* Handle optimistic insert in hidden replies

* Share root query key for easier cache extraction

* Make blurred posts not look like ass

* Fix double deleted item

* Make optimistic deleted state not look like crap in tree view

* Fix parents traversal 4 real

* Rename tree post

* Make optimistic deletions of linear posts not look bad

* Rename linear post components

* Handle tombstone views

* Rename read more component

* Add moreParents handling

* Align interaction states of read more

* Fix read more on FF

* Tree view skeleton

* Reply composer skele

* Remove hack for showing more replies

* Checkpoint: sort change scrolling fixed

* Checkpoint: learned new things, reset to base

* Feature gate

* Rename

* Replace show more

* Update settings screen

* Update pkg and endpoint

* Remove console

* Eureka

* Cleanup last commit

* No tests atm

* Remove scroll provider

* Clean up callbacks, better error state

* Remove todo

* Remove todo

* Remove todos

* Format

* Ok I think scrolling is solid

* Add back mobile compose input

* Ok need to compute headerHeight every time

* Update comments

* Ok button up web too

* Threads v2 tweaks (#8467)

* fix error screen collapsing

* use personx icon for blocked posts

* Remove height/width

* Revert unused Header change

* Clarify code

* Relate consts to theme values

* Remove debug code

* Typo

* Fix debounce of threads prefs

* Update metadata comments, dev mode

* Missed a spot

* Clean up todo

* Fix up no-unauthenticated posts

* Truncate parents if no-unauth

* Update getBranch docs

* Remove debug code

* Expand fetching in some cases

* Clear scroll need for root post to fix jump bug

* Fix reply composer skeleton state

* Remove uneeded initialized value

* Add profile shadow cache

* Some metrics

* prettier tweak

* eslint ignore

* Fix optimistic insertion

* Typo

* Rename, comment

* Remove wait

* Counter naming

* Replies seen counter for moderated sub-trees

* Remove borders on skeleton

* Align tombstone with optimistic deletion state

* Fix optimistic deletion for thread

* Add tree view icon

* Rename

* Cleanup

* Update settings copy

* Header menu open metric

* Bump package

* Better reply prompt (#8474)

* restyle reply prompt

* hide bottom bar border for cleaner look

* use new border hiding hook in DMs

* create `transparentifyColor` function

* adjust padding

* fix padding in immersive lpayer

* Apply suggestions from code review

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Integrate post-source

(cherry picked from commit fe053e9b38395a4fcb30a4367bc800f64ea84fe9)

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Diffstat (limited to 'src/state/queries/usePostThread/index.ts')
-rw-r--r--src/state/queries/usePostThread/index.ts325
1 files changed, 325 insertions, 0 deletions
diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts
new file mode 100644
index 000000000..782888cfb
--- /dev/null
+++ b/src/state/queries/usePostThread/index.ts
@@ -0,0 +1,325 @@
+import {useCallback, useMemo, useState} from 'react'
+import {useQuery, useQueryClient} from '@tanstack/react-query'
+
+import {isWeb} from '#/platform/detection'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences'
+import {
+  LINEAR_VIEW_BELOW,
+  LINEAR_VIEW_BF,
+  TREE_VIEW_BELOW,
+  TREE_VIEW_BELOW_DESKTOP,
+  TREE_VIEW_BF,
+} from '#/state/queries/usePostThread/const'
+import {
+  createCacheMutator,
+  getThreadPlaceholder,
+} from '#/state/queries/usePostThread/queryCache'
+import {
+  buildThread,
+  sortAndAnnotateThreadItems,
+} from '#/state/queries/usePostThread/traversal'
+import {
+  createPostThreadOtherQueryKey,
+  createPostThreadQueryKey,
+  type ThreadItem,
+  type UsePostThreadQueryResult,
+} from '#/state/queries/usePostThread/types'
+import {getThreadgateRecord} from '#/state/queries/usePostThread/utils'
+import * as views from '#/state/queries/usePostThread/views'
+import {useAgent, useSession} from '#/state/session'
+import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
+import {useBreakpoints} from '#/alf'
+
+export * from '#/state/queries/usePostThread/types'
+
+export function usePostThread({anchor}: {anchor?: string}) {
+  const qc = useQueryClient()
+  const agent = useAgent()
+  const {hasSession} = useSession()
+  const {gtPhone} = useBreakpoints()
+  const moderationOpts = useModerationOpts()
+  const mergeThreadgateHiddenReplies = useMergeThreadgateHiddenReplies()
+  const {
+    isLoaded: isThreadPreferencesLoaded,
+    sort,
+    setSort: baseSetSort,
+    view,
+    setView: baseSetView,
+    prioritizeFollowedUsers,
+  } = useThreadPreferences()
+  const below = useMemo(() => {
+    return view === 'linear'
+      ? LINEAR_VIEW_BELOW
+      : isWeb && gtPhone
+      ? TREE_VIEW_BELOW_DESKTOP
+      : TREE_VIEW_BELOW
+  }, [view, gtPhone])
+
+  const postThreadQueryKey = createPostThreadQueryKey({
+    anchor,
+    sort,
+    view,
+    prioritizeFollowedUsers,
+  })
+  const postThreadOtherQueryKey = createPostThreadOtherQueryKey({
+    anchor,
+    prioritizeFollowedUsers,
+  })
+
+  const query = useQuery<UsePostThreadQueryResult>({
+    enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts,
+    queryKey: postThreadQueryKey,
+    async queryFn(ctx) {
+      const {data} = await agent.app.bsky.unspecced.getPostThreadV2({
+        anchor: anchor!,
+        branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF,
+        below,
+        sort: sort,
+        prioritizeFollowedUsers: prioritizeFollowedUsers,
+      })
+
+      /*
+       * Initialize `ctx.meta` to track if we know we have additional replies
+       * we could fetch once we hit the end.
+       */
+      ctx.meta = ctx.meta || {
+        hasOtherReplies: false,
+      }
+
+      /*
+       * If we know we have additional replies, we'll set this to true.
+       */
+      if (data.hasOtherReplies) {
+        ctx.meta.hasOtherReplies = true
+      }
+
+      const result = {
+        thread: data.thread || [],
+        threadgate: data.threadgate,
+        hasOtherReplies: !!ctx.meta.hasOtherReplies,
+      }
+
+      const record = getThreadgateRecord(result.threadgate)
+      if (result.threadgate && record) {
+        result.threadgate.record = record
+      }
+
+      return result as UsePostThreadQueryResult
+    },
+    placeholderData() {
+      if (!anchor) return
+      const placeholder = getThreadPlaceholder(qc, anchor)
+      /*
+       * Always return something here, even empty data, so that
+       * `isPlaceholderData` is always true, which we'll use to insert
+       * skeletons.
+       */
+      const thread = placeholder ? [placeholder] : []
+      return {thread, threadgate: undefined, hasOtherReplies: false}
+    },
+    select(data) {
+      const record = getThreadgateRecord(data.threadgate)
+      if (data.threadgate && record) {
+        data.threadgate.record = record
+      }
+      return data
+    },
+  })
+
+  const thread = useMemo(() => query.data?.thread || [], [query.data?.thread])
+  const threadgate = useMemo(
+    () => query.data?.threadgate,
+    [query.data?.threadgate],
+  )
+  const hasOtherThreadItems = useMemo(
+    () => !!query.data?.hasOtherReplies,
+    [query.data?.hasOtherReplies],
+  )
+  const [otherItemsVisible, setOtherItemsVisible] = useState(false)
+
+  /**
+   * Creates a mutator for the post thread cache. This is used to insert
+   * replies into the thread cache after posting.
+   */
+  const mutator = useMemo(
+    () =>
+      createCacheMutator({
+        params: {view, below},
+        postThreadQueryKey,
+        postThreadOtherQueryKey,
+        queryClient: qc,
+      }),
+    [qc, view, below, postThreadQueryKey, postThreadOtherQueryKey],
+  )
+
+  /**
+   * If we have additional items available from the server and the user has
+   * chosen to view them, start loading data
+   */
+  const additionalQueryEnabled = hasOtherThreadItems && otherItemsVisible
+  const additionalItemsQuery = useQuery({
+    enabled: additionalQueryEnabled,
+    queryKey: postThreadOtherQueryKey,
+    async queryFn() {
+      const {data} = await agent.app.bsky.unspecced.getPostThreadOtherV2({
+        anchor: anchor!,
+        prioritizeFollowedUsers,
+      })
+      return data
+    },
+  })
+  const serverOtherThreadItems: ThreadItem[] = useMemo(() => {
+    if (!additionalQueryEnabled) return []
+    if (additionalItemsQuery.isLoading) {
+      return Array.from({length: 2}).map((_, i) =>
+        views.skeleton({
+          key: `other-reply-${i}`,
+          item: 'reply',
+        }),
+      )
+    } else if (additionalItemsQuery.isError) {
+      /*
+       * We could insert an special error component in here, but since these
+       * are optional additional replies, it's not critical that they're shown
+       * atm.
+       */
+      return []
+    } else if (additionalItemsQuery.data?.thread) {
+      const {threadItems} = sortAndAnnotateThreadItems(
+        additionalItemsQuery.data.thread,
+        {
+          view,
+          skipModerationHandling: true,
+          threadgateHiddenReplies: mergeThreadgateHiddenReplies(
+            threadgate?.record,
+          ),
+          moderationOpts: moderationOpts!,
+        },
+      )
+      return threadItems
+    } else {
+      return []
+    }
+  }, [
+    view,
+    additionalQueryEnabled,
+    additionalItemsQuery,
+    mergeThreadgateHiddenReplies,
+    moderationOpts,
+    threadgate?.record,
+  ])
+
+  /**
+   * Sets the sort order for the thread and resets the additional thread items
+   */
+  const setSort: typeof baseSetSort = useCallback(
+    nextSort => {
+      setOtherItemsVisible(false)
+      baseSetSort(nextSort)
+    },
+    [baseSetSort, setOtherItemsVisible],
+  )
+
+  /**
+   * Sets the view variant for the thread and resets the additional thread items
+   */
+  const setView: typeof baseSetView = useCallback(
+    nextView => {
+      setOtherItemsVisible(false)
+      baseSetView(nextView)
+    },
+    [baseSetView, setOtherItemsVisible],
+  )
+
+  /*
+   * This is the main thread response, sorted into separate buckets based on
+   * moderation, and annotated with all UI state needed for rendering.
+   */
+  const {threadItems, otherThreadItems} = useMemo(() => {
+    return sortAndAnnotateThreadItems(thread, {
+      view: view,
+      threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate?.record),
+      moderationOpts: moderationOpts!,
+    })
+  }, [
+    thread,
+    threadgate?.record,
+    mergeThreadgateHiddenReplies,
+    moderationOpts,
+    view,
+  ])
+
+  /*
+   * Take all three sets of thread items and combine them into a single thread,
+   * along with any other thread items required for rendering e.g. "Show more
+   * replies" or the reply composer.
+   */
+  const items = useMemo(() => {
+    return buildThread({
+      threadItems,
+      otherThreadItems,
+      serverOtherThreadItems,
+      isLoading: query.isPlaceholderData,
+      hasSession,
+      hasOtherThreadItems,
+      otherItemsVisible,
+      showOtherItems: () => setOtherItemsVisible(true),
+    })
+  }, [
+    threadItems,
+    otherThreadItems,
+    serverOtherThreadItems,
+    query.isPlaceholderData,
+    hasSession,
+    hasOtherThreadItems,
+    otherItemsVisible,
+    setOtherItemsVisible,
+  ])
+
+  return useMemo(
+    () => ({
+      state: {
+        /*
+         * Copy in any query state that is useful
+         */
+        isFetching: query.isFetching,
+        isPlaceholderData: query.isPlaceholderData,
+        error: query.error,
+        /*
+         * Other state
+         */
+        sort,
+        view,
+        otherItemsVisible,
+      },
+      data: {
+        items,
+        threadgate,
+      },
+      actions: {
+        /*
+         * Copy in any query actions that are useful
+         */
+        insertReplies: mutator.insertReplies,
+        refetch: query.refetch,
+        /*
+         * Other actions
+         */
+        setSort,
+        setView,
+      },
+    }),
+    [
+      query,
+      mutator.insertReplies,
+      otherItemsVisible,
+      sort,
+      view,
+      setSort,
+      setView,
+      threadgate,
+      items,
+    ],
+  )
+}