diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index 65bed7b915..69e79d4cbd 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -293,7 +293,7 @@ export function Inner({uri}: {uri: string | undefined}) { { - item.onLoad() + item.onPress() /* * Bit of a hack. This resets the ref value for the anchor so that * the next time `onContentSizeChangeWebOnly` fires, it won't diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index 43edffb4d0..e273358cc9 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -8,7 +8,10 @@ import { createCacheMutator, getThreadPlaceholder, } from '#/state/queries/usePostThread/queryCache' -import {combine, traverse} from '#/state/queries/usePostThread/traversal' +import { + buildThread, + sortAndAnnotateThreadItems, +} from '#/state/queries/usePostThread/traversal' import { createPostThreadHiddenQueryKey, createPostThreadQueryKey, @@ -119,7 +122,7 @@ export function usePostThread({anchor}: {anchor?: string}) { ) const [hiddenItemsVisible, setHiddenItemsVisible] = useState(false) - const [additionalHiddenItems, setAdditionalHiddenItems] = useState< + const [serverHiddenThreadItems, setServerHiddenThreadItems] = useState< ThreadItem[] >([]) @@ -129,10 +132,10 @@ export function usePostThread({anchor}: {anchor?: string}) { const setSort: typeof baseSetSort = useCallback( nextSort => { setHiddenItemsVisible(false) - setAdditionalHiddenItems([]) + setServerHiddenThreadItems([]) baseSetSort(nextSort) }, - [baseSetSort, setAdditionalHiddenItems, setHiddenItemsVisible], + [baseSetSort, setServerHiddenThreadItems, setHiddenItemsVisible], ) /** @@ -141,10 +144,10 @@ export function usePostThread({anchor}: {anchor?: string}) { const setView: typeof baseSetView = useCallback( nextView => { setHiddenItemsVisible(false) - setAdditionalHiddenItems([]) + setServerHiddenThreadItems([]) baseSetView(nextView) }, - [baseSetView, setAdditionalHiddenItems, setHiddenItemsVisible], + [baseSetView, setServerHiddenThreadItems, setHiddenItemsVisible], ) /** @@ -169,7 +172,7 @@ export function usePostThread({anchor}: {anchor?: string}) { * the initial visible response(s) are shown immediately. Remote data is * fetched and inserted when it's available. */ - const loadServerHiddenItems = useCallback(async () => { + const loadServerHiddenThreadItems = useCallback(async () => { /* * Show any moderated replies already in memory that were handled here on * the client. If there are server-hidden replies, we'll fetch those next. @@ -181,7 +184,7 @@ export function usePostThread({anchor}: {anchor?: string}) { */ if (!hasServerHiddenItems) return - setAdditionalHiddenItems( + setServerHiddenThreadItems( Array.from({length: 2}).map((_, i) => views.skeleton({ key: `${anchor!}-reply-${i}`, @@ -207,7 +210,7 @@ export function usePostThread({anchor}: {anchor?: string}) { }), ) - const {items} = traverse(data || [], { + const {threadItems} = sortAndAnnotateThreadItems(data || [], { view, skipHiddenReplyHandling: true, threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate?.record), @@ -215,7 +218,7 @@ export function usePostThread({anchor}: {anchor?: string}) { }) // insert the hidden replies into the state - setAdditionalHiddenItems(items) + setServerHiddenThreadItems(threadItems) }, [ qc, agent, @@ -229,90 +232,49 @@ export function usePostThread({anchor}: {anchor?: string}) { setHiddenItemsVisible, ]) - /** - * Builds the full set of thread items, minus any server-hidden replies. + /* + * This is the main thread response, sorted into separate buckets based on + * moderation, and annotated with all UI state needed for rendering. */ - const threadItems = useMemo(() => { - const traversal = traverse(thread, { + const {threadItems, hiddenThreadItems} = useMemo(() => { + return sortAndAnnotateThreadItems(thread, { view: view, threadgateHiddenReplies: mergeThreadgateHiddenReplies(threadgate?.record), moderationOpts: moderationOpts!, }) - return combine(traversal, { - hasSession, - hasServerHiddenItems, - hiddenItemsVisible, - loadServerHiddenItems, - }) }, [ thread, threadgate?.record, mergeThreadgateHiddenReplies, moderationOpts, - hasSession, view, - hasServerHiddenItems, - hiddenItemsVisible, - loadServerHiddenItems, ]) - /** - * Computes the final thread items based on load state and the availability - * of server-hidden replies. + /* + * 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 hidden + * replies" or the reply composer. */ const items = useMemo(() => { - const result = [...threadItems] - - if (query.isPlaceholderData) { - const anchorPost = result.at(0) - const skeletonReplies = - anchorPost && anchorPost.type === 'threadPost' - ? anchorPost?.value.post.replyCount ?? 4 - : 4 - - if (!result.length) { - result.push( - views.skeleton({ - key: anchor!, - item: 'anchor', - }), - ) - - if (hasSession) { - result.push( - views.skeleton({ - key: 'replyComposer', - item: 'replyComposer', - }), - ) - } - } - - for (let i = 0; i < skeletonReplies; i++) { - result.push( - views.skeleton({ - key: `${anchor!}-reply-${i}`, - item: 'reply', - }), - ) - } - - return result - } else { - result.push(...additionalHiddenItems) - result.push({ - type: 'bookend', - key: 'bookend-down', - direction: 'down', - }) - return result - } + return buildThread({ + threadItems, + hiddenThreadItems, + serverHiddenThreadItems, + isLoading: query.isPlaceholderData, + hasSession, + hasServerHiddenItems, + hiddenItemsVisible, + loadServerHiddenThreadItems, + }) }, [ - query.isPlaceholderData, threadItems, - additionalHiddenItems, - anchor, + hiddenThreadItems, + serverHiddenThreadItems, + query.isPlaceholderData, hasSession, + hasServerHiddenItems, + hiddenItemsVisible, + loadServerHiddenThreadItems, ]) return useMemo( @@ -350,7 +312,6 @@ export function usePostThread({anchor}: {anchor?: string}) { }), [ query, - items, mutator.insertReplies, hiddenItemsVisible, sort, @@ -358,6 +319,7 @@ export function usePostThread({anchor}: {anchor?: string}) { setSort, setView, threadgate, + items, ], ) } diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index d1bc50daba..2848c26099 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -14,7 +14,7 @@ import { } from '#/state/queries/usePostThread/utils' import * as views from '#/state/queries/usePostThread/views' -export function traverse( +export function sortAndAnnotateThreadItems( thread: ApiThreadItem[], { threadgateHiddenReplies, @@ -30,13 +30,13 @@ export function traverse( * post e.g. when fetching server-hidden replies. This will prevent * additional sorting or nested-branch truncation, and all replies, * regardless of moderation state, will be included in the resulting - * `items` array. + * `threadItems` array. */ skipHiddenReplyHandling?: boolean }, ) { - const items: ThreadItem[] = [] - const hidden: ThreadItem[] = [] + const threadItems: ThreadItem[] = [] + const hiddenThreadItems: ThreadItem[] = [] const metadatas = new Map() traversal: for (let i = 0; i < thread.length; i++) { @@ -64,11 +64,11 @@ export function traverse( */ } else if (item.depth === 0) { if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value)) { - items.push(views.threadPostNoUnauthenticated(item)) + threadItems.push(views.threadPostNoUnauthenticated(item)) } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)) { - items.push(views.threadPostNotFound(item)) + threadItems.push(views.threadPostNotFound(item)) } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)) { - items.push(views.threadPostBlocked(item)) + threadItems.push(views.threadPostBlocked(item)) } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { const post = views.threadPost({ uri: item.uri, @@ -77,7 +77,7 @@ export function traverse( moderationOpts, threadgateHiddenReplies, }) - items.push(post) + threadItems.push(post) parentTraversal: for (let pi = i - 1; pi >= 0; pi--) { const parent = thread[pi] @@ -85,16 +85,16 @@ export function traverse( if ( AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent.value) ) { - items.unshift(views.threadPostNoUnauthenticated(parent)) + threadItems.unshift(views.threadPostNoUnauthenticated(parent)) break parentTraversal } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value)) { - items.unshift(views.threadPostNotFound(parent)) + threadItems.unshift(views.threadPostNotFound(parent)) break parentTraversal } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value)) { - items.unshift(views.threadPostBlocked(parent)) + threadItems.unshift(views.threadPostBlocked(parent)) break parentTraversal } else if (AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) { - items.unshift( + threadItems.unshift( views.threadPost({ uri: parent.uri, depth: parent.depth, @@ -144,7 +144,7 @@ export function traverse( /* * Not moderated, need to insert it */ - items.push(post) + threadItems.push(post) /* * Update seen reply count of parent @@ -163,7 +163,7 @@ export function traverse( if (parentIsTopLevelReply) { // push branch anchor into sorted array - hidden.push(parent) + hiddenThreadItems.push(parent) // skip branch anchor in branch traversal const startIndex = branch.start + 1 @@ -206,7 +206,7 @@ export function traverse( if (childPost.isBlurred) { ci = getBranch(thread, ci, child.depth).end } else { - hidden.push(childPost) + hiddenThreadItems.push(childPost) } } else { /* @@ -228,10 +228,10 @@ export function traverse( } /* - * Both `items` and `hidden` now need to be traversed again to fully compute + * Both `threadItems` and `hiddenThreadItems` now need to be traversed again to fully compute * UI state based on collected metadata. These arrays will be muted in situ. */ - for (const subset of [items, hidden]) { + for (const subset of [threadItems, hiddenThreadItems]) { for (let i = 0; i < subset.length; i++) { const item = subset[i] const prevItem = subset.at(i - 1) @@ -363,56 +363,100 @@ export function traverse( } return { - items, - hidden, + threadItems, + hiddenThreadItems, } } -export function combine( - {items, hidden}: {items: ThreadItem[]; hidden: ThreadItem[]}, - { - hasSession, - hiddenItemsVisible, - hasServerHiddenItems, - loadServerHiddenItems, - }: { - hasSession: boolean - hiddenItemsVisible: boolean - hasServerHiddenItems: boolean - loadServerHiddenItems: () => Promise - }, -) { - const result = [...items] +export function buildThread({ + threadItems, + hiddenThreadItems, + serverHiddenThreadItems, + isLoading, + hasSession, + hiddenItemsVisible, + hasServerHiddenItems, + loadServerHiddenThreadItems, +}: { + threadItems: ThreadItem[] + hiddenThreadItems: ThreadItem[] + serverHiddenThreadItems: ThreadItem[] + isLoading: boolean + hasSession: boolean + hiddenItemsVisible: boolean + hasServerHiddenItems: boolean + loadServerHiddenThreadItems: () => Promise +}) { + /** + * `threadItems` is memoized here, so don't mutate it directly. + */ + const items = [...threadItems] - for (let i = 0; i < result.length; i++) { - const item = result[i] - if ( - item.type === 'threadPost' && - item.depth === 0 && - !item.value.post.viewer?.replyDisabled && - hasSession - ) { - result.splice(i + 1, 0, { - type: 'replyComposer', - key: 'replyComposer', - }) - break + if (isLoading) { + const anchorPost = items.at(0) + const skeletonReplies = + anchorPost && anchorPost.type === 'threadPost' + ? anchorPost?.value.post.replyCount ?? 4 + : 4 + + if (!items.length) { + items.push( + views.skeleton({ + key: 'anchor-skeleton', + item: 'anchor', + }), + ) + + if (hasSession) { + items.push( + views.skeleton({ + key: 'replyComposer', + item: 'replyComposer', + }), + ) + } + } + + for (let i = 0; i < skeletonReplies; i++) { + items.push( + views.skeleton({ + key: `anchor-skeleton-reply-${i}`, + item: 'reply', + }), + ) + } + } else { + for (let i = 0; i < items.length; i++) { + const item = items[i] + if ( + item.type === 'threadPost' && + item.depth === 0 && + !item.value.post.viewer?.replyDisabled && + hasSession + ) { + items.splice(i + 1, 0, { + type: 'replyComposer', + key: 'replyComposer', + }) + break + } + } + + if (hiddenThreadItems.length || hasServerHiddenItems) { + if (hiddenItemsVisible) { + items.push(...hiddenThreadItems) + items.push(...serverHiddenThreadItems) + } else { + items.push({ + type: 'showHiddenReplies', + key: 'showHiddenReplies', + onPress: loadServerHiddenThreadItems, + }) + } } } - if (hidden.length || hasServerHiddenItems) { - if (hiddenItemsVisible) { - result.push(...hidden) - } else { - result.push({ - type: 'showHiddenReplies', - key: 'showHiddenReplies', - onLoad: loadServerHiddenItems, - }) - } - } - - return result + return items } /** diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index fe62b81f7d..c942b08554 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -89,7 +89,7 @@ export type ThreadItem = | { type: 'showHiddenReplies' key: string - onLoad: () => Promise + onPress: () => Promise } | { type: 'readMore'