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, } }