Split up traversal and avoid multiple passes
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
createCacheMutator,
|
||||
getThreadPlaceholder,
|
||||
} from '#/state/queries/usePostThread/queryCache'
|
||||
import {traverse} from '#/state/queries/usePostThread/traversal'
|
||||
import {combine,traverse} from '#/state/queries/usePostThread/traversal'
|
||||
import {
|
||||
createPostThreadHiddenQueryKey,
|
||||
createPostThreadQueryKey,
|
||||
@@ -150,50 +150,44 @@ export function usePostThread({anchor}: {anchor?: string}) {
|
||||
}),
|
||||
)
|
||||
|
||||
const items = traverse(data || [], {
|
||||
const {items} = traverse(data || [], {
|
||||
view,
|
||||
skipHiddenReplyHandling: true,
|
||||
threadgateHiddenReplies: mergeThreadgateHiddenReplies(
|
||||
query.data?.threadgate?.record,
|
||||
),
|
||||
moderationOpts: moderationOpts!,
|
||||
hasSession,
|
||||
view,
|
||||
hasServerHiddenReplies,
|
||||
hiddenRepliesVisible,
|
||||
skipHiddenReplyHandling: true,
|
||||
loadHiddenReplies,
|
||||
})
|
||||
|
||||
// insert the hidden replies into the state
|
||||
setAdditionalHiddenItems(items)
|
||||
}, [
|
||||
qc,
|
||||
agent,
|
||||
view,
|
||||
anchor,
|
||||
prioritizeFollowedUsers,
|
||||
hasSession,
|
||||
mergeThreadgateHiddenReplies,
|
||||
moderationOpts,
|
||||
qc,
|
||||
query.data?.threadgate?.record,
|
||||
hasServerHiddenReplies,
|
||||
hiddenRepliesVisible,
|
||||
setHiddenRepliesVisible,
|
||||
])
|
||||
|
||||
const items = useMemo(() => {
|
||||
const results = traverse(query.data?.thread || [], {
|
||||
const combined = useMemo(() => {
|
||||
const traversal = traverse(query.data?.thread || [], {
|
||||
view: view,
|
||||
threadgateHiddenReplies: mergeThreadgateHiddenReplies(
|
||||
query.data?.threadgate?.record,
|
||||
),
|
||||
moderationOpts: moderationOpts!,
|
||||
})
|
||||
return combine(traversal, {
|
||||
hasSession,
|
||||
view: view,
|
||||
hasServerHiddenReplies,
|
||||
hiddenRepliesVisible,
|
||||
loadHiddenReplies,
|
||||
})
|
||||
|
||||
return results.concat(additionalHiddenItems)
|
||||
}, [
|
||||
query.data,
|
||||
mergeThreadgateHiddenReplies,
|
||||
@@ -203,9 +197,12 @@ export function usePostThread({anchor}: {anchor?: string}) {
|
||||
hasServerHiddenReplies,
|
||||
hiddenRepliesVisible,
|
||||
loadHiddenReplies,
|
||||
additionalHiddenItems,
|
||||
])
|
||||
|
||||
const items = useMemo(() => {
|
||||
return combined.concat(additionalHiddenItems)
|
||||
}, [combined, additionalHiddenItems])
|
||||
|
||||
if (query.isPlaceholderData) {
|
||||
const anchorPost = items.at(0)
|
||||
const skeletonReplies =
|
||||
|
||||
@@ -23,21 +23,20 @@ export function traverse(
|
||||
{
|
||||
threadgateHiddenReplies,
|
||||
moderationOpts,
|
||||
hasSession,
|
||||
view,
|
||||
hasServerHiddenReplies,
|
||||
hiddenRepliesVisible,
|
||||
skipHiddenReplyHandling,
|
||||
loadHiddenReplies,
|
||||
}: {
|
||||
threadgateHiddenReplies: Set<string>
|
||||
moderationOpts: ModerationOpts
|
||||
hasSession: boolean
|
||||
view: PostThreadParams['view']
|
||||
hasServerHiddenReplies: boolean
|
||||
hiddenRepliesVisible: boolean
|
||||
/**
|
||||
* Set to `true` in cases where we already know the moderation state of the
|
||||
* 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.
|
||||
*/
|
||||
skipHiddenReplyHandling?: boolean
|
||||
loadHiddenReplies: () => Promise<void>
|
||||
},
|
||||
) {
|
||||
const items: ThreadItem[] = []
|
||||
@@ -80,6 +79,7 @@ export function traverse(
|
||||
depth: item.depth,
|
||||
value: item.value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
})
|
||||
items.push(post)
|
||||
|
||||
@@ -104,6 +104,7 @@ export function traverse(
|
||||
depth: parent.depth,
|
||||
value: parent.value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -140,15 +141,12 @@ export function traverse(
|
||||
depth: item.depth,
|
||||
value: item.value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
})
|
||||
const postMod = getModerationState(post.moderation)
|
||||
const postIsHiddenByThreadgate = threadgateHiddenReplies.has(item.uri)
|
||||
const postIsModerated =
|
||||
postIsHiddenByThreadgate || postMod.blurred || postMod.muted
|
||||
|
||||
if (!postIsModerated || skipHiddenReplyHandling) {
|
||||
if (!post.isBlurred || skipHiddenReplyHandling) {
|
||||
/*
|
||||
* Not moderated, probably need to insert it
|
||||
* Not moderated, need to insert it
|
||||
*/
|
||||
items.push(post)
|
||||
|
||||
@@ -201,21 +199,15 @@ export function traverse(
|
||||
depth: child.depth,
|
||||
value: child.value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
})
|
||||
const childPostMod = getModerationState(childPost.moderation)
|
||||
const childPostIsHiddenByThreadgate =
|
||||
threadgateHiddenReplies.has(child.uri)
|
||||
|
||||
/*
|
||||
* If a child is hidden in any way, drop it an its sub-branch
|
||||
* entirely. To reveal these, the user must navigate to the
|
||||
* parent post directly.
|
||||
*/
|
||||
if (
|
||||
childPostMod.blurred ||
|
||||
childPostMod.muted ||
|
||||
childPostIsHiddenByThreadgate
|
||||
) {
|
||||
if (childPost.isBlurred) {
|
||||
ci = getBranch(thread, ci, child.depth).end
|
||||
} else {
|
||||
hidden.push(childPost)
|
||||
@@ -239,38 +231,17 @@ export function traverse(
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipHiddenReplyHandling) {
|
||||
if (hidden.length || hasServerHiddenReplies) {
|
||||
if (hiddenRepliesVisible) {
|
||||
items.push(...hidden)
|
||||
} else {
|
||||
items.push({
|
||||
type: 'showHiddenReplies',
|
||||
key: 'showHiddenReplies',
|
||||
onLoad: loadHiddenReplies,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
const prevItem = items.at(i - 1)
|
||||
const nextItem = items.at(i + 1)
|
||||
/*
|
||||
* Both `items` and `hidden` 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 (let i = 0; i < subset.length; i++) {
|
||||
const item = subset[i]
|
||||
const prevItem = subset.at(i - 1)
|
||||
const nextItem = subset.at(i + 1)
|
||||
|
||||
if (item.type === 'threadPost') {
|
||||
if (
|
||||
item.depth === 0 &&
|
||||
!item.value.post.viewer?.replyDisabled &&
|
||||
hasSession
|
||||
) {
|
||||
items.splice(i + 1, 0, {
|
||||
type: 'replyComposer',
|
||||
key: 'replyComposer',
|
||||
})
|
||||
i++ // skip next iteration
|
||||
}
|
||||
|
||||
const metadata = metadatas.get(item.uri)
|
||||
|
||||
if (metadata) {
|
||||
@@ -288,7 +259,8 @@ export function traverse(
|
||||
* based on the actual data that we've seen.
|
||||
*/
|
||||
metadata.isLastSibling =
|
||||
metadata.replyIndex === metadata.parentMetadata.repliesSeenCount - 1
|
||||
metadata.replyIndex ===
|
||||
metadata.parentMetadata.repliesSeenCount - 1
|
||||
metadata.isLastChild =
|
||||
metadata.nextItemDepth === undefined ||
|
||||
metadata.nextItemDepth <= metadata.depth
|
||||
@@ -359,7 +331,7 @@ export function traverse(
|
||||
*/
|
||||
if (metadata.repliesUnhydrated > 0 && metadata.isLastChild) {
|
||||
metadata.precedesChildReadMore = true
|
||||
items.splice(i + 1, 0, views.readMore(metadata))
|
||||
subset.splice(i + 1, 0, views.readMore(metadata))
|
||||
i++ // skip next iteration
|
||||
}
|
||||
|
||||
@@ -377,7 +349,7 @@ export function traverse(
|
||||
metadata.upcomingParentReadMore.depth &&
|
||||
metadata.isLastChild
|
||||
) {
|
||||
items.splice(
|
||||
subset.splice(
|
||||
i + 1,
|
||||
0,
|
||||
views.readMore(metadata.upcomingParentReadMore),
|
||||
@@ -392,6 +364,55 @@ export function traverse(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
hidden,
|
||||
}
|
||||
}
|
||||
|
||||
export function combine(
|
||||
{items, hidden}: {items: ThreadItem[]; hidden: ThreadItem[]},
|
||||
{
|
||||
hasSession,
|
||||
hiddenRepliesVisible,
|
||||
hasServerHiddenReplies,
|
||||
loadHiddenReplies,
|
||||
}: {
|
||||
hasSession: boolean
|
||||
hiddenRepliesVisible: boolean
|
||||
hasServerHiddenReplies: boolean
|
||||
loadHiddenReplies: () => Promise<void>
|
||||
},
|
||||
) {
|
||||
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 (hidden.length || hasServerHiddenReplies) {
|
||||
if (hiddenRepliesVisible) {
|
||||
return items.concat(hidden)
|
||||
} else {
|
||||
return items.concat({
|
||||
type: 'showHiddenReplies',
|
||||
key: 'showHiddenReplies',
|
||||
onLoad: loadHiddenReplies,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export type ThreadItem =
|
||||
record: AppBskyFeedPost.Record
|
||||
}
|
||||
}
|
||||
isBlurred: boolean
|
||||
moderation: ModerationDecision
|
||||
ui: {
|
||||
isAnchor: boolean
|
||||
|
||||
@@ -63,12 +63,20 @@ export function threadPost({
|
||||
depth,
|
||||
value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
}: {
|
||||
uri: string
|
||||
depth: number
|
||||
value: $Typed<AppBskyUnspeccedDefs.ThreadItemPost>
|
||||
moderationOpts: ModerationOpts
|
||||
threadgateHiddenReplies: Set<string>
|
||||
}): Extract<ThreadItem, {type: 'threadPost'}> {
|
||||
const moderation = moderatePost(value.post, moderationOpts)
|
||||
const modui = moderation.ui('contentList')
|
||||
const blurred = modui.blur || modui.filter
|
||||
const muted = (modui.blurs[0] || modui.filters[0])?.type === 'muted'
|
||||
const hiddenByThreadgate = threadgateHiddenReplies.has(uri)
|
||||
const isBlurred = hiddenByThreadgate || blurred || muted
|
||||
return {
|
||||
type: 'threadPost',
|
||||
key: uri,
|
||||
@@ -84,7 +92,8 @@ export function threadPost({
|
||||
record: AppBskyFeedPost.Record
|
||||
},
|
||||
},
|
||||
moderation: moderatePost(value.post, moderationOpts),
|
||||
isBlurred,
|
||||
moderation,
|
||||
// @ts-ignore populated by the traversal
|
||||
ui: {},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user