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
@@ -24,11 +24,11 @@ export function ReadMore({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const isTreeView = view === 'tree' const isTreeView = view === 'tree'
const indent = Math.max(0, item.indent - 1) const indent = Math.max(0, item.depth - 1)
const spacers = isTreeView const spacers = isTreeView
? Array.from(Array(indent)).map((_, n: number) => { ? Array.from(Array(indent)).map((_, n: number) => {
const isSkipped = item.skippedIndents.has(n) const isSkipped = item.skippedIndentIndices.has(n)
return ( return (
<View <View
key={`${item.key}-padding-${n}`} key={`${item.key}-padding-${n}`}
@@ -79,7 +79,11 @@ export function ReadMore({
]}> ]}>
<Trans> <Trans>
Read {item.moreReplies} more{' '} Read {item.moreReplies} more{' '}
<Plural one="reply" other="replies" value={item.moreReplies} /> <Plural
one="reply"
other="replies"
value={item.moreReplies}
/>
</Trans> </Trans>
</Text> </Text>
</> </>
@@ -193,7 +193,7 @@ let PostThreadItemLoaded = ({
], ],
]}> ]}>
{Array.from(Array(indents)).map((_, n: number) => { {Array.from(Array(indents)).map((_, n: number) => {
const isSkipped = item.ui.skippedIndents.has(n) const isSkipped = item.ui.skippedIndentIndices.has(n)
return ( return (
<View <View
key={`${post.uri}-padding-${n}`} key={`${post.uri}-padding-${n}`}
+24 -25
View File
@@ -14,6 +14,7 @@ import {
getPostRecord, getPostRecord,
getThreadPostUI, getThreadPostUI,
getTraversalMetadata, getTraversalMetadata,
storeTraversalMetadata,
} from '#/state/queries/usePostThread/utils' } from '#/state/queries/usePostThread/utils'
import * as views from '#/state/queries/usePostThread/views' import * as views from '#/state/queries/usePostThread/views'
@@ -23,7 +24,6 @@ export function flatten(
hasSession, hasSession,
showMuted, showMuted,
showHidden, showHidden,
view,
}: { }: {
hasSession: boolean hasSession: boolean
showMuted: boolean showMuted: boolean
@@ -107,9 +107,6 @@ export function sort(
const muted: Slice[] = [] const muted: Slice[] = []
const metadatas = new Map<string, TraversalMetadata>() const metadatas = new Map<string, TraversalMetadata>()
// @ts-ignore
window.__data = metadatas // for debugging
traversal: for (let i = 0; i < thread.length; i++) { traversal: for (let i = 0; i < thread.length; i++) {
const item = thread[i] const item = thread[i]
let parentMetadata: TraversalMetadata | undefined let parentMetadata: TraversalMetadata | undefined
@@ -125,8 +122,7 @@ export function sort(
prevItem: thread.at(i - 1), prevItem: thread.at(i - 1),
nextItem: thread.at(i + 1), nextItem: thread.at(i + 1),
}) })
metadatas.set(item.uri, metadata) storeTraversalMetadata(metadatas, metadata)
metadatas.set(metadata.text, metadata) // TODO debugging
} }
if (item.depth < 0) { if (item.depth < 0) {
@@ -213,10 +209,10 @@ export function sort(
} else if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) { } else if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) {
if (parentMetadata) { if (parentMetadata) {
/* /*
* Set this value before incrementing the parent's seenReplies * Set this value before incrementing the parent's repliesSeenCount
*/ */
metadata!.replyIndex = parentMetadata.seenReplies metadata!.repliesIndex = parentMetadata.repliesSeenCount
parentMetadata.seenReplies += 1 parentMetadata.repliesSeenCount += 1
} }
const post = views.threadPost({ const post = views.threadPost({
@@ -268,15 +264,15 @@ export function sort(
nextItem: thread[ci + 1], nextItem: thread[ci + 1],
parentMetadata: childParentMetadata, parentMetadata: childParentMetadata,
}) })
storeTraversalMetadata(metadatas, childMetadata)
if (childParentMetadata) { if (childParentMetadata) {
/* /*
* Set this value before incrementing the parent's seenReplies * Set this value before incrementing the parent's repliesSeenCount
*/ */
childMetadata!.replyIndex = childParentMetadata.seenReplies childMetadata!.repliesIndex =
childParentMetadata.seenReplies += 1 childParentMetadata.repliesSeenCount
childParentMetadata.repliesSeenCount += 1
} }
metadatas.set(item.uri, childMetadata)
metadatas.set(childMetadata.text, childMetadata) // TODO debugging
const childPost = views.threadPost({ const childPost = views.threadPost({
uri: child.uri, uri: child.uri,
@@ -332,8 +328,8 @@ export function sort(
/* /*
* Copy in the parent's skipped indents * Copy in the parent's skipped indents
*/ */
metadata.skippedIndents = new Set([ metadata.skippedIndentIndices = new Set([
...metadata.parentMetadata.skippedIndents, ...metadata.parentMetadata.skippedIndentIndices,
]) ])
/* /*
@@ -341,20 +337,22 @@ export function sort(
* we've seen. * we've seen.
*/ */
metadata.isLastSibling = 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 * If this is the last sibling, it's implicitly part of the last
* branch of this sub-tree. * branch of this sub-tree.
*/ */
if (metadata.isLastSibling) { 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 the parent is part of the last branch of the sub-tree, so is the child.
*/ */
if (metadata.parentMetadata.isPartOfLastBranchAtDepth) { if (metadata.parentMetadata.isPartOfLastBranchFromDepth) {
metadata.isPartOfLastBranchAtDepth = metadata.parentMetadata.isPartOfLastBranchAtDepth 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". * at some point down the line we will need to show a "read more".
*/ */
if ( if (
metadata.parentMetadata.unhydratedReplies > 0 && metadata.parentMetadata.repliesUnhydrated > 0 &&
metadata.isLastSibling metadata.isLastSibling
) { ) {
metadata.upcomingParentReadMore = metadata.parentMetadata metadata.upcomingParentReadMore = metadata.parentMetadata
@@ -383,7 +381,7 @@ export function sort(
* replies, then we know we can skip an indent line. * replies, then we know we can skip an indent line.
*/ */
if ( if (
metadata.parentMetadata.unhydratedReplies <= 0 && metadata.parentMetadata.repliesUnhydrated <= 0 &&
metadata.isLastSibling metadata.isLastSibling
) { ) {
/** /**
@@ -391,7 +389,7 @@ export function sort(
* bc of how we render these. So instead of handling that in the * bc of how we render these. So instead of handling that in the
* component, we just adjust that back to 0-index here. * 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 * If this post has unhydrated replies, and it is the last child, then
* it itself needs a "read more" * 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)) items.splice(i + 1, 0, views.readMore(metadata))
i++ // skip next iteration i++ // skip next iteration
} }
@@ -411,7 +409,8 @@ export function sort(
*/ */
if ( if (
metadata.upcomingParentReadMore && metadata.upcomingParentReadMore &&
metadata.isPartOfLastBranchAtDepth === metadata.upcomingParentReadMore.depth && metadata.isPartOfLastBranchFromDepth ===
metadata.upcomingParentReadMore.depth &&
metadata.isLastChild metadata.isLastChild
) { ) {
items.splice( items.splice(
+58 -23
View File
@@ -2,7 +2,6 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyUnspeccedGetPostThreadV2, type AppBskyUnspeccedGetPostThreadV2,
type AtUri,
type ModerationDecision, type ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
@@ -50,9 +49,8 @@ export type Slice =
showParentReplyLine: boolean showParentReplyLine: boolean
showChildReplyLine: boolean showChildReplyLine: boolean
indent: number indent: number
parentHasBranchingReplies: boolean
isLastChild: boolean isLastChild: boolean
skippedIndents: Set<number> skippedIndentIndices: Set<number>
/** /**
* Populated during the final traversal of the thread. Denotes whether * Populated during the final traversal of the thread. Denotes whether
* there is a "Read more" link for the parent immediately following * there is a "Read more" link for the parent immediately following
@@ -100,45 +98,82 @@ export type Slice =
| { | {
type: 'readMore' type: 'readMore'
key: string key: string
indent: number depth: number
href: string href: string
moreReplies: number moreReplies: number
skippedIndents: Set<number> skippedIndentIndices: Set<number>
} }
export type TraversalMetadata = { 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 depth: number
replies: number
unhydratedReplies: number
/** /**
* The number of replies that have been seen so far in the traversal. After * Indicates if the post is the last reply beneath its parent post.
* traverssal, we can use this to calculate if we actually got all the
* replies we expected, or if some were blocked, etc.
*/ */
seenReplies: number
/**
* The index-0-based index of this reply in the parent post's replies.
*/
replyIndex: number
hasBranchingReplies: boolean
isLastSibling: boolean isLastSibling: boolean
/** /**
* Indicates the post is the end-of-the-line for a given branch of replies. * Indicates the post is the end-of-the-line for a given branch of replies.
*/ */
isLastChild: boolean isLastChild: boolean
/** /**
* This is a live reference to the parent metadata object. Mutations to this * Indicates if the post is the left/lower-most branch of the reply tree.
* are available for later use in children. * 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. * The depth of the slice immediately preceding this one, if it exists.
*/ */
prevItemDepth?: number 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 parentMetadata?: TraversalMetadata
skippedIndents: Set<number> /**
[key: string]: any * 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)) { if (!AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) {
throw new Error(`Expected thread item to be a post`) throw new Error(`Expected thread item to be a post`)
} }
const replies = item.value.post.replyCount || 0 const repliesCount = item.value.post.replyCount || 0
const unhydratedReplies = item.value.moreReplies || 0 const repliesUnhydrated = item.value.moreReplies || 0
/** const metadata = {
* 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,
depth: item.depth, 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 <= * 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 * 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 * Unknown until after traversal
*/ */
isLastSibling: false, 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 if (__DEV__) {
text: getPostRecord(item.value.post).text, // @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({ export function getThreadPostUI({
depth, depth,
replies, repliesCount,
parentMetadata,
prevItemDepth, prevItemDepth,
isLastChild, isLastChild,
skippedIndents, skippedIndentIndices,
seenReplies, repliesSeenCount,
unhydratedReplies, repliesUnhydrated,
}: TraversalMetadata): Extract<Slice, {type: 'threadPost'}>['ui'] { }: 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 { return {
isAnchor: depth === 0, isAnchor: depth === 0,
showParentReplyLine: showParentReplyLine:
!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth, !!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth,
showChildReplyLine: depth < 0 || isReplyAndHasReplies, showChildReplyLine: depth < 0 || isReplyAndHasReplies,
indent: depth, indent: depth,
parentHasBranchingReplies: !!parentMetadata?.hasBranchingReplies,
/* /*
* If there are no slices below this one, or the next slice is less * If there are no slices below this one, or the next slice is less
* indented than the computed indent for this post. * indented than the computed indent for this post.
*/ */
isLastChild, //nextItemDepth === undefined || nextItemDepth < depth, isLastChild, //nextItemDepth === undefined || nextItemDepth < depth,
skippedIndents, skippedIndentIndices,
} }
} }
+15 -13
View File
@@ -9,7 +9,10 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {makeProfileLink} from '#/lib/routes/links' 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({ export function threadPostNoUnauthenticated({
uri, uri,
@@ -82,7 +85,7 @@ export function threadPost({
...value, ...value,
/* /*
* Do not spread anything here, load bearing for post shadow strict * Do not spread anything here, load bearing for post shadow strict
* equality checks. * equality reference checks.
*/ */
post: value.post as Omit<AppBskyFeedDefs.PostView, 'record'> & { post: value.post as Omit<AppBskyFeedDefs.PostView, 'record'> & {
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
@@ -95,28 +98,27 @@ export function threadPost({
} }
export function readMore({ export function readMore({
uri, depth,
authorHandle, repliesUnhydrated,
unhydratedReplies: moreReplies, skippedIndentIndices,
depth: indent, postData,
skippedIndents,
}: TraversalMetadata): Extract<Slice, {type: 'readMore'}> { }: TraversalMetadata): Extract<Slice, {type: 'readMore'}> {
const urip = new AtUri(uri) const urip = new AtUri(postData.uri)
const href = makeProfileLink( const href = makeProfileLink(
{ {
did: urip.host, did: urip.host,
handle: authorHandle, handle: postData.authorHandle,
}, },
'post', 'post',
urip.rkey, urip.rkey,
) )
return { return {
type: 'readMore' as const, type: 'readMore' as const,
key: `readMore:${uri}`, key: `readMore:${postData.uri}`,
href, href,
moreReplies, moreReplies: repliesUnhydrated,
indent, depth,
skippedIndents, skippedIndentIndices,
} }
} }