From c1e986c44633a0903df4eae850eb6d20eb27739b Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 19 Dec 2025 07:20:30 +0400 Subject: [PATCH] implement collapsing thread replies --- .../[did]/[publication]/[rkey]/ThreadPage.tsx | 71 +++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx b/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx index 51e62ed4..a841a25e 100644 --- a/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx @@ -1,4 +1,5 @@ "use client"; +import { useEffect, useRef } from "react"; import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api"; import useSWR, { preload } from "swr"; import { PageWrapper } from "components/Pages/Page"; @@ -16,6 +17,8 @@ import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { useHasPageLoaded } from "components/InitialPageLoadProvider"; import { openPage, OpenPage } from "./PostPages"; import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; +import { useThreadState } from "src/useThreadState"; +import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded"; type ThreadViewPost = AppBskyFeedDefs.ThreadViewPost; type NotFoundPost = AppBskyFeedDefs.NotFoundPost; @@ -120,6 +123,17 @@ export function ThreadPage(props: { function ThreadContent(props: { thread: ThreadType; threadUri: string }) { const { thread, threadUri } = props; + const mainPostRef = useRef(null); + + // Scroll the main post into view when the thread loads + useEffect(() => { + if (mainPostRef.current) { + mainPostRef.current.scrollIntoView({ + behavior: "instant", + block: "start", + }); + } + }, []); if (AppBskyFeedDefs.isNotFoundPost(thread)) { return ; @@ -160,12 +174,14 @@ function ThreadContent(props: { thread: ThreadType; threadUri: string }) { ))} {/* Main post */} - +
+ +
{/* Replies */} {thread.replies && thread.replies.length > 0 && ( @@ -280,6 +296,8 @@ function Replies(props: { depth: number; }) { const { replies, threadUri, depth } = props; + const collapsedThreads = useThreadState((s) => s.collapsedThreads); + const toggleCollapsed = useThreadState((s) => s.toggleCollapsed); return (
@@ -311,6 +329,8 @@ function Replies(props: { } const hasReplies = reply.replies && reply.replies.length > 0; + const isCollapsed = collapsedThreads.has(reply.post.uri); + const replyCount = reply.replies?.length ?? 0; return (
@@ -321,12 +341,39 @@ function Replies(props: { threadUri={threadUri} /> {hasReplies && depth < 3 && ( -
- +
+ {/* Clickable collapse line - w-8 matches avatar width, centered line aligns with avatar center */} + + {isCollapsed ? ( + + ) : ( +
+ +
+ )}
)} {hasReplies && depth >= 3 && ( -- 2.51.2