Clean it up

This commit is contained in:
Eric Bailey
2025-05-29 15:52:54 -05:00
parent 24b6b9ea56
commit 98711ebc2b
6 changed files with 171 additions and 118 deletions
+21 -17
View File
@@ -24,25 +24,25 @@ export function ReadMore({
const t = useTheme()
const {_} = useLingui()
const isTreeView = view === 'tree'
const indent = Math.max(0, item.indent - 1)
const indent = Math.max(0, item.depth - 1)
const spacers = isTreeView
? Array.from(Array(indent)).map((_, n: number) => {
const isSkipped = item.skippedIndents.has(n)
return (
<View
key={`${item.key}-padding-${n}`}
style={[
t.atoms.border_contrast_low,
{
borderRightWidth: isSkipped ? 0 : REPLY_LINE_WIDTH,
width: TREE_INDENT + TREE_AVI_WIDTH / 2,
left: 1,
},
]}
/>
)
})
const isSkipped = item.skippedIndentIndices.has(n)
return (
<View
key={`${item.key}-padding-${n}`}
style={[
t.atoms.border_contrast_low,
{
borderRightWidth: isSkipped ? 0 : REPLY_LINE_WIDTH,
width: TREE_INDENT + TREE_AVI_WIDTH / 2,
left: 1,
},
]}
/>
)
})
: null
return (
@@ -79,7 +79,11 @@ export function ReadMore({
]}>
<Trans>
Read {item.moreReplies} more{' '}
<Plural one="reply" other="replies" value={item.moreReplies} />
<Plural
one="reply"
other="replies"
value={item.moreReplies}
/>
</Trans>
</Text>
</>
@@ -193,7 +193,7 @@ let PostThreadItemLoaded = ({
],
]}>
{Array.from(Array(indents)).map((_, n: number) => {
const isSkipped = item.ui.skippedIndents.has(n)
const isSkipped = item.ui.skippedIndentIndices.has(n)
return (
<View
key={`${post.uri}-padding-${n}`}
+24 -25
View File
@@ -14,6 +14,7 @@ import {
getPostRecord,
getThreadPostUI,
getTraversalMetadata,
storeTraversalMetadata,
} from '#/state/queries/usePostThread/utils'
import * as views from '#/state/queries/usePostThread/views'
@@ -23,7 +24,6 @@ export function flatten(
hasSession,
showMuted,
showHidden,
view,
}: {
hasSession: boolean
showMuted: boolean
@@ -107,9 +107,6 @@ export function sort(
const muted: Slice[] = []
const metadatas = new Map<string, TraversalMetadata>()
// @ts-ignore
window.__data = metadatas // for debugging
traversal: for (let i = 0; i < thread.length; i++) {
const item = thread[i]
let parentMetadata: TraversalMetadata | undefined
@@ -125,8 +122,7 @@ export function sort(
prevItem: thread.at(i - 1),
nextItem: thread.at(i + 1),
})
metadatas.set(item.uri, metadata)
metadatas.set(metadata.text, metadata) // TODO debugging
storeTraversalMetadata(metadatas, metadata)
}
if (item.depth < 0) {
@@ -213,10 +209,10 @@ export function sort(
} else if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) {
if (parentMetadata) {
/*
* Set this value before incrementing the parent's seenReplies
* Set this value before incrementing the parent's repliesSeenCount
*/
metadata!.replyIndex = parentMetadata.seenReplies
parentMetadata.seenReplies += 1
metadata!.repliesIndex = parentMetadata.repliesSeenCount
parentMetadata.repliesSeenCount += 1
}
const post = views.threadPost({
@@ -268,15 +264,15 @@ export function sort(
nextItem: thread[ci + 1],
parentMetadata: childParentMetadata,
})
storeTraversalMetadata(metadatas, childMetadata)
if (childParentMetadata) {
/*
* Set this value before incrementing the parent's seenReplies
* Set this value before incrementing the parent's repliesSeenCount
*/
childMetadata!.replyIndex = childParentMetadata.seenReplies
childParentMetadata.seenReplies += 1
childMetadata!.repliesIndex =
childParentMetadata.repliesSeenCount
childParentMetadata.repliesSeenCount += 1
}
metadatas.set(item.uri, childMetadata)
metadatas.set(childMetadata.text, childMetadata) // TODO debugging
const childPost = views.threadPost({
uri: child.uri,
@@ -332,8 +328,8 @@ export function sort(
/*
* Copy in the parent's skipped indents
*/
metadata.skippedIndents = new Set([
...metadata.parentMetadata.skippedIndents,
metadata.skippedIndentIndices = new Set([
...metadata.parentMetadata.skippedIndentIndices,
])
/*
@@ -341,20 +337,22 @@ export function sort(
* we've seen.
*/
metadata.isLastSibling =
metadata.replyIndex === metadata.parentMetadata.seenReplies - 1
metadata.repliesIndex ===
metadata.parentMetadata.repliesSeenCount - 1
/*
* If this is the last sibling, it's implicitly part of the last
* branch of this sub-tree.
*/
if (metadata.isLastSibling) {
metadata.isPartOfLastBranchAtDepth = metadata.depth
metadata.isPartOfLastBranchFromDepth = metadata.depth
/**
* If the parent is part of the last branch of the sub-tree, so is the child.
*/
if (metadata.parentMetadata.isPartOfLastBranchAtDepth) {
metadata.isPartOfLastBranchAtDepth = metadata.parentMetadata.isPartOfLastBranchAtDepth
if (metadata.parentMetadata.isPartOfLastBranchFromDepth) {
metadata.isPartOfLastBranchFromDepth =
metadata.parentMetadata.isPartOfLastBranchFromDepth
}
}
@@ -363,7 +361,7 @@ export function sort(
* at some point down the line we will need to show a "read more".
*/
if (
metadata.parentMetadata.unhydratedReplies > 0 &&
metadata.parentMetadata.repliesUnhydrated > 0 &&
metadata.isLastSibling
) {
metadata.upcomingParentReadMore = metadata.parentMetadata
@@ -383,7 +381,7 @@ export function sort(
* replies, then we know we can skip an indent line.
*/
if (
metadata.parentMetadata.unhydratedReplies <= 0 &&
metadata.parentMetadata.repliesUnhydrated <= 0 &&
metadata.isLastSibling
) {
/**
@@ -391,7 +389,7 @@ export function sort(
* bc of how we render these. So instead of handling that in the
* component, we just adjust that back to 0-index here.
*/
metadata.skippedIndents.add(item.depth - 2)
metadata.skippedIndentIndices.add(item.depth - 2)
}
}
@@ -399,7 +397,7 @@ export function sort(
* If this post has unhydrated replies, and it is the last child, then
* it itself needs a "read more"
*/
if (metadata.unhydratedReplies > 0 && metadata.isLastChild) {
if (metadata.repliesUnhydrated > 0 && metadata.isLastChild) {
items.splice(i + 1, 0, views.readMore(metadata))
i++ // skip next iteration
}
@@ -411,7 +409,8 @@ export function sort(
*/
if (
metadata.upcomingParentReadMore &&
metadata.isPartOfLastBranchAtDepth === metadata.upcomingParentReadMore.depth &&
metadata.isPartOfLastBranchFromDepth ===
metadata.upcomingParentReadMore.depth &&
metadata.isLastChild
) {
items.splice(
+58 -23
View File
@@ -2,7 +2,6 @@ import {
type AppBskyFeedDefs,
type AppBskyFeedPost,
type AppBskyUnspeccedGetPostThreadV2,
type AtUri,
type ModerationDecision,
} from '@atproto/api'
@@ -50,9 +49,8 @@ export type Slice =
showParentReplyLine: boolean
showChildReplyLine: boolean
indent: number
parentHasBranchingReplies: boolean
isLastChild: boolean
skippedIndents: Set<number>
skippedIndentIndices: Set<number>
/**
* Populated during the final traversal of the thread. Denotes whether
* there is a "Read more" link for the parent immediately following
@@ -100,45 +98,82 @@ export type Slice =
| {
type: 'readMore'
key: string
indent: number
depth: number
href: string
moreReplies: number
skippedIndents: Set<number>
skippedIndentIndices: Set<number>
}
export type TraversalMetadata = {
/**
* The depth of the post in the reply tree, where 0 is the root post. This is
* calculated on the server.
*/
depth: number
replies: number
unhydratedReplies: number
/**
* The number of replies that have been seen so far in the traversal. After
* traverssal, we can use this to calculate if we actually got all the
* replies we expected, or if some were blocked, etc.
* Indicates if the post is the last reply beneath its parent post.
*/
seenReplies: number
/**
* The index-0-based index of this reply in the parent post's replies.
*/
replyIndex: number
hasBranchingReplies: boolean
isLastSibling: boolean
/**
* Indicates the post is the end-of-the-line for a given branch of replies.
*/
isLastChild: boolean
/**
* This is a live reference to the parent metadata object. Mutations to this
* are available for later use in children.
* Indicates if the post is the left/lower-most branch of the reply tree.
* Value corresponds to the depth at which this branch started.
*/
parentMetadata?: TraversalMetadata
isPartOfLastBranchFromDepth?: number
/**
* The depth of the slice immediately following this one, if it exists.
*/
nextItemDepth?: number
/**
* The depth of the slice immediately preceding this one, if it exists.
*/
prevItemDepth?: number
/**
* The depth of the slice immediately following this one, if it exists.
* This is a live reference to the parent metadata object. Mutations to this
* are available for later use in children.
*/
nextItemDepth?: number
skippedIndents: Set<number>
[key: string]: any
parentMetadata?: TraversalMetadata
/**
* Any data needed to be passed along to the "read more" items. Keep this
* trim for better memory usage.
*/
postData: {
uri: string
authorHandle: string
}
/**
* The total number of replies to this post, including those not hydrated
* and returned by the response.
*/
repliesCount: number
/**
* The number of replies to this post not hydrated and returned by the
* response.
*/
repliesUnhydrated: number
/**
* The number of replies that have been seen so far in the traversal. After
* traverssal, we can use this to calculate if we actually got all the
* replies we expected, or if some were blocked, etc.
*/
repliesSeenCount: number
/**
* The index-0-based index of this reply in the parent post's replies.
*/
repliesIndex: number
/**
* Each slice is responsible for rendering reply lines based on its depth.
* This value corresponds to any line indices that can be skipped e.g.
* because there are no further replies below this sub-tree to render.
*/
skippedIndentIndices: Set<number>
/**
* Indicates and stores parent data IF that parent has additional unhydrated
* replies. This value is passed down to children along the left/lower-most
* branch of the tree. When the end is reached, a "read more" is inserted.
*/
upcomingParentReadMore?: TraversalMetadata
}
+52 -39
View File
@@ -54,36 +54,10 @@ export function getTraversalMetadata({
if (!AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) {
throw new Error(`Expected thread item to be a post`)
}
const replies = item.value.post.replyCount || 0
const unhydratedReplies = item.value.moreReplies || 0
/**
* If the post has more than a single reply, and the total reply count
* minus the number of replies not present in the response is greater than
* 1, then we must have more than a single branch of replies present in the
* response, which can affect how we render tree view.
*/
const hasBranchingReplies = replies > 1 && replies - unhydratedReplies > 1
return {
uri: item.uri,
const repliesCount = item.value.post.replyCount || 0
const repliesUnhydrated = item.value.moreReplies || 0
const metadata = {
depth: item.depth,
authorHandle: item.value.post.author.handle,
replies,
unhydratedReplies,
seenReplies: 0,
replyIndex: 0,
hasBranchingReplies,
parentMetadata,
isTopLevelReply: item.depth === 1,
skippedIndents: new Set(),
prevItemDepth: prevItem?.depth,
nextItemDepth: nextItem?.depth,
/*
* If it's a top level reply, bc we render each top-level branch as a
* separate tree, it's implicitly part of the last branch. For subsequent
* replies, we'll override this after traversal.
*/
isPartOfLastBranchAtDepth: item.depth === 1 ? 1 : undefined,
/*
* If there are no slices below this one, or the next slice has a depth <=
* than the depth of this post, it's the last child of the reply tree. It
@@ -95,35 +69,74 @@ export function getTraversalMetadata({
* Unknown until after traversal
*/
isLastSibling: false,
/*
* If it's a top level reply, bc we render each top-level branch as a
* separate tree, it's implicitly part of the last branch. For subsequent
* replies, we'll override this after traversal.
*/
isPartOfLastBranchFromDepth: item.depth === 1 ? 1 : undefined,
nextItemDepth: nextItem?.depth,
prevItemDepth: prevItem?.depth,
parentMetadata,
postData: {
uri: item.uri,
authorHandle: item.value.post.author.handle,
},
repliesCount,
repliesUnhydrated,
repliesSeenCount: 0,
repliesIndex: 0,
skippedIndentIndices: new Set<number>(),
}
// TODO non-spec
text: getPostRecord(item.value.post).text,
if (__DEV__) {
// @ts-ignore dev only for debugging
metadata.postData.text = getPostRecord(item.value.post).text
}
return metadata
}
export function storeTraversalMetadata(
metadatas: Map<string, TraversalMetadata>,
metadata: TraversalMetadata,
) {
metadatas.set(metadata.postData.uri, metadata)
if (__DEV__) {
// @ts-ignore dev only for debugging
metadatas.set(metadata.postData.text, metadata)
// @ts-ignore
window.__thread = metadatas
}
}
export function getThreadPostUI({
depth,
replies,
parentMetadata,
repliesCount,
prevItemDepth,
isLastChild,
skippedIndents,
seenReplies,
unhydratedReplies,
skippedIndentIndices,
repliesSeenCount,
repliesUnhydrated,
}: TraversalMetadata): Extract<Slice, {type: 'threadPost'}>['ui'] {
const isReplyAndHasReplies = depth > 0 && replies > 0 && ((replies - unhydratedReplies) === seenReplies || seenReplies > 0)
// TODO might be able to simplify this
const isReplyAndHasReplies =
depth > 0 &&
repliesCount > 0 &&
(repliesCount - repliesUnhydrated === repliesSeenCount ||
repliesSeenCount > 0)
return {
isAnchor: depth === 0,
showParentReplyLine:
!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth,
showChildReplyLine: depth < 0 || isReplyAndHasReplies,
indent: depth,
parentHasBranchingReplies: !!parentMetadata?.hasBranchingReplies,
/*
* If there are no slices below this one, or the next slice is less
* indented than the computed indent for this post.
*/
isLastChild, //nextItemDepth === undefined || nextItemDepth < depth,
skippedIndents,
skippedIndentIndices,
}
}
+15 -13
View File
@@ -9,7 +9,10 @@ import {
} from '@atproto/api'
import {makeProfileLink} from '#/lib/routes/links'
import {type Slice, type TraversalMetadata} from '#/state/queries/usePostThread/types'
import {
type Slice,
type TraversalMetadata,
} from '#/state/queries/usePostThread/types'
export function threadPostNoUnauthenticated({
uri,
@@ -82,7 +85,7 @@ export function threadPost({
...value,
/*
* Do not spread anything here, load bearing for post shadow strict
* equality checks.
* equality reference checks.
*/
post: value.post as Omit<AppBskyFeedDefs.PostView, 'record'> & {
record: AppBskyFeedPost.Record
@@ -95,28 +98,27 @@ export function threadPost({
}
export function readMore({
uri,
authorHandle,
unhydratedReplies: moreReplies,
depth: indent,
skippedIndents,
depth,
repliesUnhydrated,
skippedIndentIndices,
postData,
}: TraversalMetadata): Extract<Slice, {type: 'readMore'}> {
const urip = new AtUri(uri)
const urip = new AtUri(postData.uri)
const href = makeProfileLink(
{
did: urip.host,
handle: authorHandle,
handle: postData.authorHandle,
},
'post',
urip.rkey,
)
return {
type: 'readMore' as const,
key: `readMore:${uri}`,
key: `readMore:${postData.uri}`,
href,
moreReplies,
indent,
skippedIndents,
moreReplies: repliesUnhydrated,
depth,
skippedIndentIndices,
}
}