diff --git a/src/screens/PostThread/components/ReadMore.tsx b/src/screens/PostThread/components/ReadMore.tsx
index ee7a97706e..6171776d86 100644
--- a/src/screens/PostThread/components/ReadMore.tsx
+++ b/src/screens/PostThread/components/ReadMore.tsx
@@ -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 (
-
- )
- })
+ const isSkipped = item.skippedIndentIndices.has(n)
+ return (
+
+ )
+ })
: null
return (
@@ -79,7 +79,11 @@ export function ReadMore({
]}>
Read {item.moreReplies} more{' '}
-
+
>
diff --git a/src/screens/PostThread/components/ThreadReply.tsx b/src/screens/PostThread/components/ThreadReply.tsx
index 65255abe21..6300a919d7 100644
--- a/src/screens/PostThread/components/ThreadReply.tsx
+++ b/src/screens/PostThread/components/ThreadReply.tsx
@@ -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 (
()
- // @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(
diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts
index 4a71345d73..1361afcbe0 100644
--- a/src/state/queries/usePostThread/types.ts
+++ b/src/state/queries/usePostThread/types.ts
@@ -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
+ skippedIndentIndices: Set
/**
* 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
+ skippedIndentIndices: Set
}
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
- [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
+ /**
+ * 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
}
diff --git a/src/state/queries/usePostThread/utils.ts b/src/state/queries/usePostThread/utils.ts
index 00ca21b208..91f7042626 100644
--- a/src/state/queries/usePostThread/utils.ts
+++ b/src/state/queries/usePostThread/utils.ts
@@ -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(),
+ }
- // 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,
+ 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['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,
}
}
diff --git a/src/state/queries/usePostThread/views.ts b/src/state/queries/usePostThread/views.ts
index 945a57ca20..e10bd656b3 100644
--- a/src/state/queries/usePostThread/views.ts
+++ b/src/state/queries/usePostThread/views.ts
@@ -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 & {
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 {
- 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,
}
}