diff options
author | Eric Bailey <git@esb.lol> | 2025-06-11 14:32:14 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-06-11 14:32:14 -0500 |
commit | 61004b887b0c7515837e051144b694fc7db5a1cc (patch) | |
tree | 08cda716a97867480996f21d384824987fe3c15b /src/state/queries/usePostThread/index.ts | |
parent | 143d5f3b814f1ce707fdfc87dabff7af5349bd06 (diff) | |
download | voidsky-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.ts | 325 |
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, + ], + ) +} |