Compare commits

...

9 Commits

Author SHA1 Message Date
Eric Bailey 8f641f8a1b WIP sorta working op threads 2025-10-14 20:29:43 -05:00
Eric Bailey c428568b0c Move indent calc to metadata, add ui field to readMore, use indent 2025-10-14 16:22:59 -05:00
Eric Bailey df9c289f55 Hide labels on posts unless post is anchor 2025-10-14 16:22:59 -05:00
Eric Bailey 6a84088dba Fix isRoot calc 2025-10-14 16:22:59 -05:00
Eric Bailey 99fbe2b680 WIP OP continuous thread 2025-10-14 16:22:59 -05:00
Eric Bailey 92f58c8723 The counters serve the same purpose, we only need to know the count of the actual replies rendered to the view in order to calculate the replyIndex 2025-10-14 16:21:14 -05:00
Eric Bailey fceec83e08 isLastSiblingByCounts isn't needed, should rely only on the count of replies seen 2025-10-14 16:21:14 -05:00
Eric Bailey b928e27c18 Update comments 2025-10-14 16:21:14 -05:00
Eric Bailey 8d4ce43301 Fix computation of isLastSibling and isLastChild to account for muted or
otherwise hidden replies
2025-10-14 16:21:14 -05:00
7 changed files with 156 additions and 60 deletions
@@ -294,12 +294,16 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
postHref={postHref}
style={[a.pb_xs]}
/>
<LabelsOnMyPost post={post} style={[a.pb_xs]} />
<PostAlerts
modui={moderation.ui('contentList')}
style={[a.pb_2xs]}
additionalCauses={additionalPostAlerts}
/>
{item.ui.isAnchor && (
<>
<LabelsOnMyPost post={post} style={[a.pb_xs]} />
<PostAlerts
modui={moderation.ui('contentList')}
style={[a.pb_2xs]}
additionalCauses={additionalPostAlerts}
/>
</>
)}
{richText?.text ? (
<>
<RichText
@@ -28,7 +28,7 @@ export const ThreadItemReadMore = memo(function ThreadItemReadMore({
const t = useTheme()
const {_} = useLingui()
const isTreeView = view === 'tree'
const indent = Math.max(0, item.depth - 1)
const indent = Math.max(0, item.ui.indent - 1)
const spacers = isTreeView
? Array.from(Array(indent)).map((_, n: number) => {
@@ -334,12 +334,16 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
<View style={[a.flex_row]}>
<ThreadItemTreeReplyChildReplyLine item={item} />
<View style={[a.flex_1, a.pl_2xs]}>
<LabelsOnMyPost post={post} style={[a.pb_2xs]} />
<PostAlerts
modui={moderation.ui('contentList')}
style={[a.pb_2xs]}
additionalCauses={additionalPostAlerts}
/>
{item.ui.isAnchor && (
<>
<LabelsOnMyPost post={post} style={[a.pb_2xs]} />
<PostAlerts
modui={moderation.ui('contentList')}
style={[a.pb_2xs]}
additionalCauses={additionalPostAlerts}
/>
</>
)}
{richText?.text ? (
<>
<RichText
+53 -32
View File
@@ -9,6 +9,7 @@ import {
} from '#/state/queries/usePostThread/types'
import {
getPostRecord,
getReadMoreUI,
getThreadPostNoUnauthenticatedUI,
getThreadPostUI,
getTraversalMetadata,
@@ -135,11 +136,11 @@ export function sortAndAnnotateThreadItems(
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
if (parentMetadata) {
/*
* Set this value before incrementing the parent's repliesSeenCounter
* Set this value before incrementing the `repliesSeenCounter` later
* on, since `repliesSeenCounter` is 1-indexed and `replyIndex` is
* 0-indexed.
*/
metadata!.replyIndex = parentMetadata.repliesIndexCounter
// Increment the parent's repliesIndexCounter
parentMetadata.repliesIndexCounter += 1
metadata!.replyIndex = parentMetadata.repliesSeenCounter
}
const post = views.threadPost({
@@ -151,6 +152,18 @@ export function sortAndAnnotateThreadItems(
})
if (!post.isBlurred || skipModerationHandling) {
const skip =
parentMetadata?.isPartOfOPThreadFromRoot &&
!parentMetadata.isEndOfOPThreadFromRoot &&
!metadata?.isPartOfOPThreadFromRoot
if (skip) {
const branch = getBranch(thread, i, item.depth)
// could insert tombstone
i = branch.end
continue traversal
}
/*
* Not moderated, need to insert it
*/
@@ -193,11 +206,12 @@ export function sortAndAnnotateThreadItems(
storeTraversalMetadata(metadatas, childMetadata)
if (childParentMetadata) {
/*
* Set this value before incrementing the parent's repliesIndexCounter
* Set this value before incrementing the
* `repliesSeenCounter` later on, since `repliesSeenCounter`
* is 1-indexed and `replyIndex` is 0-indexed.
*/
childMetadata!.replyIndex =
childParentMetadata.repliesIndexCounter
childParentMetadata.repliesIndexCounter += 1
childParentMetadata.repliesSeenCounter
}
const childPost = views.threadPost({
@@ -264,14 +278,13 @@ export function sortAndAnnotateThreadItems(
if (nextItem?.type === 'threadPost')
metadata.nextItemDepth = nextItem?.depth
/*
* Item is the last "sibling" if we know for sure we're out of
* replies on the parent (even though this item itself may have its
* own reply branches).
/**
* Item is also the last "sibling" if its index matches the total
* number of replies we're actually able to render to the page.
*/
const isLastSiblingByCounts =
const isLastSiblingDueToMissingReplies =
metadata.replyIndex ===
metadata.parentMetadata.repliesIndexCounter - 1
metadata.parentMetadata.repliesSeenCounter - 1
/*
* Item can also be the last "sibling" if we know we don't have a
@@ -280,6 +293,7 @@ export function sortAndAnnotateThreadItems(
* item).
*/
const isImplicitlyLastSibling =
nextItem === undefined ||
metadata.nextItemDepth === undefined ||
metadata.nextItemDepth < metadata.depth
@@ -287,7 +301,7 @@ export function sortAndAnnotateThreadItems(
* Ok now we can set the last sibling state.
*/
metadata.isLastSibling =
isLastSiblingByCounts || isImplicitlyLastSibling
isImplicitlyLastSibling || isLastSiblingDueToMissingReplies
/*
* Item is the last "child" in a branch if there is no next item,
@@ -296,6 +310,7 @@ export function sortAndAnnotateThreadItems(
* of this item)
*/
metadata.isLastChild =
nextItem === undefined ||
metadata.nextItemDepth === undefined ||
metadata.nextItemDepth <= metadata.depth
@@ -350,19 +365,25 @@ export function sortAndAnnotateThreadItems(
])
/**
* If this is the last sibling, and the parent has no unhydrated
* replies, then we know we can skip an indent line.
* If the parent has no unhydrated replies that may require a
* `readMore`, we may be able to skip some of the indent lines.
*/
if (
metadata.parentMetadata.repliesUnhydrated <= 0 &&
metadata.isLastSibling
) {
/**
* Depth is 2 more than the 0-index of the indent calculation
* bc of how we render these. So instead of handling that in the
* component, we just adjust that back to 0-index here.
*/
metadata.skippedIndentIndices.add(item.depth - 2)
if (metadata.parentMetadata.repliesUnhydrated <= 0) {
if (metadata.isLastSibling) {
/**
* We should definitely have a "last sibling" at this point.
*
* Note: depth is 2 more than the 0-index of the indent
* calculation bc of how we render these. So instead of
* handling that in the component, we just adjust that back to
* 0-index here.
*/
metadata.skippedIndentIndices.add(item.depth - 2)
} else if (metadata.parentMetadata?.isEndOfOPThreadFromRoot) {
metadata.skippedIndentIndices.delete(
metadata.parentMetadata.indent - 1,
)
}
}
}
@@ -372,7 +393,9 @@ export function sortAndAnnotateThreadItems(
*/
if (metadata.repliesUnhydrated > 0 && metadata.isLastChild) {
metadata.precedesChildReadMore = true
subset.splice(i + 1, 0, views.readMore(metadata))
const view = views.readMore(metadata)
view.ui = getReadMoreUI(metadata)
subset.splice(i + 1, 0, view)
i++ // skip next iteration
}
@@ -395,11 +418,9 @@ export function sortAndAnnotateThreadItems(
(metadata.nextItemDepth === undefined ||
metadata.nextItemDepth <= metadata.upcomingParentReadMore.depth)
) {
subset.splice(
i + 1,
0,
views.readMore(metadata.upcomingParentReadMore),
)
const view = views.readMore(metadata.upcomingParentReadMore)
view.ui = getReadMoreUI(metadata.upcomingParentReadMore)
subset.splice(i + 1, 0, view)
i++
}
+35 -13
View File
@@ -106,6 +106,9 @@ export type ThreadItem =
href: string
moreReplies: number
skippedIndentIndices: Set<number>
ui: {
indent: number
}
}
| {
/*
@@ -137,6 +140,12 @@ export type TraversalMetadata = {
* calculated on the server.
*/
depth: number
/**
* The visual indentation level of the post. This is usually the same as
* `depth`, except in the case where the post is part of the OP thread,
* which is always indented to level 1 (except the root post, which is 0).
*/
indent: number
/**
* Indicates if this item is a "read more" link preceding this post that
* continues the thread upwards.
@@ -155,6 +164,18 @@ export type TraversalMetadata = {
* tree. Value corresponds to the depth at which this branch started.
*/
isPartOfLastBranchFromDepth?: number
/**
* Indicates if the post is part of the thread started by the original
* poster, starting from the root post. OP threads within other branches
* of the tree are not considered part of the OP thread.
*/
isPartOfOPThreadFromRoot: boolean
isEndOfOPThreadFromRoot: boolean
/**
* Indicates if this item is the root post of the thread.
*/
isRootPost: boolean
endOfOPThreadDepthOffset: number
/**
* The depth of the slice immediately following this one, if it exists.
*/
@@ -193,23 +214,24 @@ export type TraversalMetadata = {
*/
repliesUnhydrated: number
/**
* The number of replies that have been seen so far in the traversal.
* Excludes replies that are moderated in some way, since those are not
* "seen" on first load. Use `repliesIndexCounter` for the total number of
* replies that were hydrated in the response.
* The number of replies that have been "seen" (actually able to be rendered)
* so far in the traversal. Excludes replies that are moderated in some way,
* since those are not "seen" on first load.
*
* After traversal, we can use this to calculate if we actually got all the
* replies we expected, or if some were blocked, etc.
* We use this to compute the `replyIndex` values of the children of this
* parent. E.g. if a reply is not hydrated on the response, or is moderated
* in some way (including by the user), this value is not incremented. So
* this represents the _actual_ index of the reply in the rendered view.
*
* Note: this is a "counter", not an "index". Because this value is
* incremented starting from 0, it is 1-indexed. So to when comparing to the
* `replyIndex`, you'll need to subtract 1 from this value.
*/
repliesSeenCounter: number
/**
* The total number of replies to this post hydrated in this response. Used
* for populating the `replyIndex` of the post by referencing this value on
* the parent.
*/
repliesIndexCounter: number
/**
* The index-0-based index of this reply in the parent post's replies.
* The index-0-based index of this reply in the parent post's replies. This
* is computed from the `repliesSeenCounter` of the parent post, prior to it
* being incremented for this reply.
*/
replyIndex: number
/**
+45 -2
View File
@@ -63,10 +63,40 @@ export function getTraversalMetadata({
if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
throw new Error(`Expected thread item to be a post`)
}
const rootPostUri = getRootPostAtUri(item.value.post)
const isRootPost = item.uri === rootPostUri?.toString()
const rootPostAuthorDid = rootPostUri?.host
const currentPostAuthorDid = item.value.post.author.did
const currentPostIsByRootAuthor = currentPostAuthorDid === rootPostAuthorDid
const nextPostAuthorDid =
nextItem && AppBskyUnspeccedDefs.isThreadItemPost(nextItem.value)
? nextItem.value.post.author.did
: undefined
const parentIsPartOfOPThreadFromRoot = Boolean(
parentMetadata?.isRootPost || parentMetadata?.isPartOfOPThreadFromRoot,
)
const isPartOfOPThreadFromRoot =
currentPostIsByRootAuthor && parentIsPartOfOPThreadFromRoot
const isEndOfOPThreadFromRoot =
isPartOfOPThreadFromRoot && currentPostAuthorDid !== nextPostAuthorDid
let indent = item.depth
if (isPartOfOPThreadFromRoot) {
indent = item.depth > 1 ? 1 : 0
} else if (parentMetadata?.endOfOPThreadDepthOffset) {
indent = item.depth - parentMetadata?.endOfOPThreadDepthOffset
}
// minus 1 so that the next reply under the end of the OP thread is indented by 1
const endOfOPThreadDepthOffset = isEndOfOPThreadFromRoot ? item.depth - 1 : 0
const repliesCount = item.value.post.replyCount || 0
const repliesUnhydrated = item.value.moreReplies || 0
const metadata = {
depth: item.depth,
indent,
/*
* Unknown until after traversal
*/
@@ -81,6 +111,11 @@ export function getTraversalMetadata({
* replies, we'll override this after traversal.
*/
isPartOfLastBranchFromDepth: item.depth === 1 ? 1 : undefined,
isPartOfOPThreadFromRoot,
isEndOfOPThreadFromRoot,
isRootPost,
endOfOPThreadDepthOffset:
parentMetadata?.endOfOPThreadDepthOffset || endOfOPThreadDepthOffset,
nextItemDepth: nextItem?.depth,
parentMetadata,
prevItemDepth: prevItem?.depth,
@@ -99,7 +134,6 @@ export function getTraversalMetadata({
repliesCount,
repliesUnhydrated,
repliesSeenCounter: 0,
repliesIndexCounter: 0,
replyIndex: 0,
skippedIndentIndices: new Set<number>(),
}
@@ -128,6 +162,7 @@ export function storeTraversalMetadata(
export function getThreadPostUI({
depth,
indent,
repliesCount,
prevItemDepth,
isLastChild,
@@ -148,7 +183,7 @@ export function getThreadPostUI({
followsReadMoreUp ||
(!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth),
showChildReplyLine: depth < 0 || isReplyAndHasReplies,
indent: depth,
indent,
/*
* 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
@@ -174,3 +209,11 @@ export function getThreadPostNoUnauthenticatedUI({
showParentReplyLine: Boolean(prevItemDepth && prevItemDepth < depth),
}
}
export function getReadMoreUI({
indent,
}: TraversalMetadata): Extract<ThreadItem, {type: 'readMore'}>['ui'] {
return {
indent,
}
}
+2
View File
@@ -124,6 +124,8 @@ export function readMore({
moreReplies: repliesUnhydrated,
depth,
skippedIndentIndices,
// @ts-ignore populated by the traversal
ui: {},
}
}