diff --git a/src/screens/PostThread/components/ReadMore.tsx b/src/screens/PostThread/components/ReadMore.tsx index a6b6564db3..ee7a97706e 100644 --- a/src/screens/PostThread/components/ReadMore.tsx +++ b/src/screens/PostThread/components/ReadMore.tsx @@ -2,7 +2,6 @@ import {View} from 'react-native' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {makeProfileLink} from '#/lib/routes/links' import {type PostThreadParams, type Slice} from '#/state/queries/usePostThread' import { LINEAR_AVI_WIDTH, @@ -28,19 +27,22 @@ export function ReadMore({ const indent = Math.max(0, item.indent - 1) const spacers = isTreeView - ? Array.from(Array(indent)).map((_, n: number) => ( + ? Array.from(Array(indent)).map((_, n: number) => { + const isSkipped = item.skippedIndents.has(n) + return ( - )) + ) + }) : null return ( @@ -63,14 +65,7 @@ export function ReadMore({ /> {({hovered, pressed}) => { return ( @@ -83,8 +78,8 @@ export function ReadMore({ (hovered || pressed) && a.underline, ]}> - Read {item.replyCount} more{' '} - + Read {item.moreReplies} more{' '} + diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index b553399dc5..8c4b509359 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -41,7 +41,7 @@ export function usePostThread({ const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ anchor: params.anchor!, branchingFactor: params.view === 'linear' ? 1 : 3, // 100 TODO - below: 6, + below: 3, sort: params.sort, prioritizeFollowedUsers: params.prioritizeFollowedUsers, }) diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 537cc2532e..6c7d9e331c 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -32,7 +32,6 @@ export function flatten( }, ) { const flattened: Slice[] = sorted.items - const parents = [] for (let i = 0; i < flattened.length; i++) { const item = flattened[i] @@ -49,103 +48,6 @@ export function flatten( key: 'replyComposer', }) } - - const deepestParent = parents[parents.length - 1] - - if (deepestParent) { - // next item is a sibling or an aunt/uncle - if (item.depth <= deepestParent.depth) { - for (let pi = parents.length - 1; pi >= 0; pi--) { - const parent = parents[pi] - - if (item.depth <= parent.depth) { - /* - * Find the previous post item and set the read more flags - */ - for (let ui = i - 1; ui >= 0; ui--) { - let prev = flattened[ui] - if (prev.type === 'threadPost') { - prev.ui.precedesParentReadMore = - prev.ui.indent - 1 === parent.ui.indent // true - prev.ui.precedesChildReadMore = - prev.ui.indent === item.ui.indent - break - } - } - - flattened.splice( - i + 1 + (pi - parents.length), - 0, - views.readMore({ - parent, - }), - ) - parents.pop() - - // skip next iteration - i++ - - if (view === 'linear') { - break - } - } else { - break - } - } - } - } - - if (item.value.moreReplies > 0) { - parents.push(item) - } - - const isLastIteration = i === flattened.length - 1 - - if (isLastIteration) { - const deepestParent = parents[parents.length - 1] - - if (deepestParent) { - // next item is a sibling or an aunt/uncle - if (deepestParent.depth <= item.depth) { - for (let pi = parents.length - 1; pi >= 0; pi--) { - const parent = parents[pi] - if (parent.depth <= item.depth) { - /* - * Find the previous post item and set the read more flags - */ - for (let ui = i; ui >= 0; ui--) { - let prev = flattened[ui] - if (prev.type === 'threadPost') { - prev.ui.precedesParentReadMore = - prev.ui.indent - 1 === parent.ui.indent - prev.ui.precedesChildReadMore = - prev.ui.indent === item.ui.indent - break - } - } - - flattened.splice( - i + 2 + (pi - parents.length), - 0, - views.readMore({ - parent, - }), - ) - parents.pop() - - // skip next iteration - i++ - - if (view === 'linear') { - break - } - } else { - break - } - } - } - } - } } } @@ -310,10 +212,23 @@ export function sort( continue traversal } else if (AppBskyUnspeccedGetPostThreadV2.isThreadItemPost(item.value)) { if (parentMetadata) { + if (metadata) { + metadata.replyIndex = parentMetadata.seenReplies + } + parentMetadata.seenReplies += 1 + if (metadata) { metadata.isLastSibling = - parentMetadata.replies === parentMetadata.seenReplies + parentMetadata.replies - parentMetadata.unhydratedReplies === + parentMetadata.seenReplies + + if ( + parentMetadata.unhydratedReplies > 0 && + metadata.isLastSibling + ) { + metadata.upcomingParentReadMore = parentMetadata + } } } @@ -369,8 +284,16 @@ export function sort( if (childParentMetadata) { childParentMetadata.seenReplies += 1 childMetadata.isLastSibling = - childParentMetadata.replies === + childParentMetadata.replies - + childParentMetadata.unhydratedReplies === childParentMetadata.seenReplies + + if ( + childParentMetadata.unhydratedReplies > 0 && + childMetadata.isLastSibling + ) { + childMetadata.upcomingParentReadMore = childParentMetadata + } } metadatas.set(item.uri, childMetadata) metadatas.set(childMetadata.text, childMetadata) // TODO debugging @@ -418,7 +341,9 @@ export function sort( } } - for (const item of items) { + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (item.type === 'threadPost') { const metadata = metadatas.get(item.uri) if (metadata) { @@ -426,9 +351,31 @@ export function sort( metadata.skippedIndents = new Set([ ...metadata.parentMetadata.skippedIndents, ]) + + if ( + metadata.isLastSibling && + metadata.parentMetadata.unhydratedReplies <= 0 + ) { + metadata.skippedIndents.add(item.depth - 2) + } } - if (metadata.isLastSibling) { - metadata.skippedIndents.add(item.depth - 2) + + if ( + metadata.unhydratedReplies > 0 && + (metadata.nextItemDepth === undefined || + metadata.nextItemDepth <= item.depth) + ) { + items.splice(i + 1, 0, views.readMore(metadata)) + i++ + } + + if (metadata.upcomingParentReadMore && metadata.isDeadEnd) { + items.splice( + i + 1, + 0, + views.readMore(metadata.upcomingParentReadMore), + ) + i++ } item.ui = getThreadPostUI(metadata) @@ -436,8 +383,6 @@ export function sort( } } - // console.log(metadatas) - return { items, hidden, diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index 8d509ce43a..2b9a360f59 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -101,14 +101,13 @@ export type Slice = type: 'readMore' key: string indent: number - replyCount: number - nextAnchor: Extract - nextAnchorUri: AtUri + href: string + moreReplies: number + skippedIndents: Set } export type TraversalMetadata = { depth: number - indent: number replies: number unhydratedReplies: number seenReplies: number diff --git a/src/state/queries/usePostThread/utils.ts b/src/state/queries/usePostThread/utils.ts index 509c340bf2..2800ace92b 100644 --- a/src/state/queries/usePostThread/utils.ts +++ b/src/state/queries/usePostThread/utils.ts @@ -65,11 +65,9 @@ export function getTraversalMetadata({ const hasBranchingReplies = replies > 1 && replies - unhydratedReplies > 1 return { + uri: item.uri, depth: item.depth, - // TODO maybe not used - indent: parentMetadata?.hasBranchingReplies - ? item.depth - : parentMetadata?.indent || item.depth, + authorHandle: item.value.post.author.handle, replies, unhydratedReplies, seenReplies: 0, @@ -79,6 +77,13 @@ export function getTraversalMetadata({ skippedIndents: new Set(), prevItemDepth: prevItem?.depth, nextItemDepth: nextItem?.depth, + /* + * If there are no slices below this one, or the next slice is less + * indented than the computed indent for this post. + */ + isDeadEnd: nextItem?.depth === undefined || nextItem?.depth < item.depth, + + upcomingParentReadMore: parentMetadata?.upcomingParentReadMore || undefined, // TODO non-spec text: getPostRecord(item.value.post).text, @@ -87,25 +92,27 @@ export function getTraversalMetadata({ export function getThreadPostUI({ depth, - indent, replies, parentMetadata, prevItemDepth, - nextItemDepth, + isDeadEnd, skippedIndents, + seenReplies, + unhydratedReplies, }: TraversalMetadata): Extract['ui'] { + const isReplyAndHasReplies = depth > 0 && replies > 0 && ((replies - unhydratedReplies) === seenReplies || seenReplies > 0) return { isAnchor: depth === 0, showParentReplyLine: !!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth, - showChildReplyLine: replies > 0, + 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. */ - isDeadEnd: nextItemDepth === undefined || nextItemDepth < indent, + isDeadEnd, //nextItemDepth === undefined || nextItemDepth < depth, skippedIndents, } } diff --git a/src/state/queries/usePostThread/views.ts b/src/state/queries/usePostThread/views.ts index 8619332e24..945a57ca20 100644 --- a/src/state/queries/usePostThread/views.ts +++ b/src/state/queries/usePostThread/views.ts @@ -8,7 +8,8 @@ import { type ModerationOpts, } from '@atproto/api' -import {type Slice} from '#/state/queries/usePostThread/types' +import {makeProfileLink} from '#/lib/routes/links' +import {type Slice, type TraversalMetadata} from '#/state/queries/usePostThread/types' export function threadPostNoUnauthenticated({ uri, @@ -94,19 +95,28 @@ export function threadPost({ } export function readMore({ - parent, -}: { - parent: Extract -}) { + uri, + authorHandle, + unhydratedReplies: moreReplies, + depth: indent, + skippedIndents, +}: TraversalMetadata): Extract { + const urip = new AtUri(uri) + const href = makeProfileLink( + { + did: urip.host, + handle: authorHandle, + }, + 'post', + urip.rkey, + ) return { type: 'readMore' as const, - key: `readMore:${parent.uri}`, - indent: parent.ui.parentHasBranchingReplies - ? parent.depth - : parent.ui.indent, - replyCount: parent.value.moreReplies, - nextAnchor: parent, - nextAnchorUri: new AtUri(parent.uri), + key: `readMore:${uri}`, + href, + moreReplies, + indent, + skippedIndents, } }