diff --git a/src/screens/PostThread/__tests__/reader.test.ts b/src/screens/PostThread/__tests__/reader.test.ts
index 46f4cf5be..97ad755c5 100644
--- a/src/screens/PostThread/__tests__/reader.test.ts
+++ b/src/screens/PostThread/__tests__/reader.test.ts
@@ -93,16 +93,19 @@ describe('buildReaderThread', () => {
expect(segments[0].seam.expanded).toBe(false)
expect(segments[0].seam.hiddenReplyCount).toBe(3)
expect(segments[0].seam.continuationUri).toBe(two.uri)
+ expect(segments[0].seam.isThreadEnd).toBe(false)
// the last seam counts all replies, since none render below it
expect(segments[1].item).toBe(two)
expect(segments[1].seam.hiddenReplyCount).toBe(2)
expect(segments[1].seam.continuationUri).toBe('')
+ expect(segments[1].seam.isThreadEnd).toBe(true)
- // the anchor seam counts all of its replies and continues into the chain
+ // the anchor seam excludes the continuation already shown as a segment
expect(anchorSeam).toBeDefined()
expect(anchorSeam?.expanded).toBe(false)
- expect(anchorSeam?.hiddenReplyCount).toBe(4)
+ expect(anchorSeam?.hiddenReplyCount).toBe(3)
expect(anchorSeam?.continuationUri).toBe(one.uri)
+ expect(anchorSeam?.isThreadEnd).toBe(false)
expect(expandedSeam).toBeUndefined()
})
@@ -176,7 +179,7 @@ describe('buildReaderThread', () => {
expect(expandedSeam).toBe(segments[1].seam)
})
- it('keeps a trailing read more with the chain', () => {
+ it('attaches a trailing read more to the last segment', () => {
const anchor = post({rkey: 'a', depth: 0})
const one = post({rkey: 'b', depth: 1, opThread: true})
const readMore: ThreadItem = {
@@ -193,11 +196,30 @@ describe('buildReaderThread', () => {
NO_SEAMS,
)
- expect(items.map(i => i.type)).toEqual([
- 'threadPost',
- 'readerSegment',
- 'readMore',
- ])
+ expect(items.map(i => i.type)).toEqual(['threadPost', 'readerSegment'])
+ const segments = segmentsOf(items)
+ expect(segments[0].seam.isThreadEnd).toBe(false)
+ expect(segments[0].trailingReadMore).toBe(readMore)
+ })
+
+ it('hides trailing read more while the last seam is expanded', () => {
+ const anchor = post({rkey: 'a', depth: 0})
+ const one = post({rkey: 'b', depth: 1, opThread: true})
+ const readMore: ThreadItem = {
+ type: 'readMore',
+ key: `readMore:${one.uri}`,
+ depth: 2,
+ href: '/x',
+ moreReplies: 5,
+ skippedIndentIndices: new Set(),
+ }
+ const {items} = buildReaderThread([anchor, one, readMore], {
+ expandedSeamUri: one.uri,
+ })
+
+ const segments = segmentsOf(items)
+ expect(segments[0].seam.expanded).toBe(true)
+ expect(segments[0].trailingReadMore).toBeUndefined()
})
it('drops sibling replies even when the chain is not the first sibling', () => {
diff --git a/src/screens/PostThread/components/ReaderSeam.tsx b/src/screens/PostThread/components/ReaderSeam.tsx
index c841efbc4..994c38fd7 100644
--- a/src/screens/PostThread/components/ReaderSeam.tsx
+++ b/src/screens/PostThread/components/ReaderSeam.tsx
@@ -53,6 +53,7 @@ export function ReaderSeam({
continuationUri,
href,
sort,
+ isThreadEnd = false,
onToggle,
onPostSuccess,
threadgateRecord,
@@ -63,6 +64,11 @@ export function ReaderSeam({
continuationUri: string
href: string
sort: string
+ /**
+ * Last post in the OP chain with no "Read more replies" below. Collapsed
+ * seams get a modest trailing spacer (smaller than the expanded-reply gap).
+ */
+ isThreadEnd?: boolean
onToggle: () => void
onPostSuccess?: (data: OnPostSuccessData) => void
threadgateRecord?: AppBskyFeedThreadgate.Record
@@ -82,6 +88,7 @@ export function ReaderSeam({
continuationUri={continuationUri}
href={href}
sort={sort}
+ isThreadEnd={isThreadEnd}
onToggle={onToggle}
onPostSuccess={onPostSuccess}
threadgateRecord={threadgateRecord}
@@ -97,6 +104,7 @@ function ReaderSeamInner({
continuationUri,
href,
sort,
+ isThreadEnd,
onToggle,
onPostSuccess,
threadgateRecord,
@@ -108,6 +116,7 @@ function ReaderSeamInner({
continuationUri: string
href: string
sort: string
+ isThreadEnd: boolean
onToggle: () => void
onPostSuccess?: (data: OnPostSuccessData) => void
threadgateRecord?: AppBskyFeedThreadgate.Record
@@ -323,7 +332,15 @@ function ReaderSeamInner({
/>
)}
- {expanded && }
+ {/*
+ * Expanded: same gap as between consecutive reader segments.
+ * Collapsed thread end: smaller spacer before Also liked / page end.
+ */}
+ {expanded ? (
+
+ ) : (
+ isThreadEnd &&
+ )}
>
)
}
diff --git a/src/screens/PostThread/components/ReaderSeamControls.tsx b/src/screens/PostThread/components/ReaderSeamControls.tsx
index 342ef6f1d..3f18a2f13 100644
--- a/src/screens/PostThread/components/ReaderSeamControls.tsx
+++ b/src/screens/PostThread/components/ReaderSeamControls.tsx
@@ -56,7 +56,8 @@ import * as bsky from '#/types/bsky'
* plus any expanded details and replies. `left` positions it relative to the
* consumer's container, since the anchor renders inside padded content while
* segments render full bleed. `bottom` lets the consumer raise the bottom cap
- * to line up with the seam's interaction row.
+ * to line up with the seam's interaction row when collapsed, or sit above the
+ * trailing spacer / on the replies' end rule when expanded.
*/
export function ReaderBracket({
left,
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index cd4c24f6f..00011bc0b 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -60,7 +60,7 @@ import {
type ReaderSeam as ReaderSeamData,
type ThreadPostPosition,
} from '#/screens/PostThread/reader'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, tokens, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
@@ -484,7 +484,11 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
)}
@@ -531,7 +535,13 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
{post.embed && (