From 2b1f4ee5bcae3ae7a9c0f83eba07a2deb6e3ec4a Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Thu, 24 Sep 2026 22:35:39 -0400 Subject: [PATCH] feat(threads): "View Thread (N replies)" on a thread's root message (#165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK gives a thread root its own id as threadRootId, so the `!isThreadedMessage` guard hid the thread action on exactly the message that has a thread; the "N replies" chip was the only way in. Roots now get the action in both the hover bar and the ⋮ menu, labelled "View thread" / "View Thread (14 replies)" with the thread icon; other messages keep "Reply in Thread". Same handler, which opens the thread panel. Verified in Chromium: on a 14-reply root both entries show and open the panel; an ordinary message still shows "Reply in thread". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/room/message/Message.tsx | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index c61cba5da..8a5cb3426 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -1176,6 +1176,18 @@ export const Message = React.memo( }; const isThreadedMessage = mEvent.threadRootId !== undefined; + // [Gitea #165] A root that already has a thread: the same action opens it, + // so say "View Thread (N replies)" rather than "Reply in Thread". + // The SDK gives a thread root its own id as threadRootId, so without this + // a root (the one message that HAS a thread) got no thread action at all. + const isThreadRoot = !!mEvent.getId() && mEvent.threadRootId === mEvent.getId(); + const showThreadAction = !isThreadedMessage || isThreadRoot; + const existingThread = mEvent.getId() ? room.getThread(mEvent.getId()!) : null; + const threadReplies = existingThread?.length ?? 0; + const threadActionLabel = + threadReplies > 0 + ? `View Thread (${threadReplies} ${threadReplies === 1 ? 'reply' : 'replies'})` + : 'Reply in Thread'; // The full action menu, shared by the desktop PopOut and the touch // bottom sheet (#166). @@ -1273,10 +1285,12 @@ export const Message = React.memo( )} - {!isThreadedMessage && ( + {showThreadAction && ( } + after={ + 0 ? Icons.Thread : Icons.ThreadPlus} size="100" /> + } radii="300" data-event-id={mEvent.getId()} onClick={(evt: React.MouseEvent) => { @@ -1285,7 +1299,7 @@ export const Message = React.memo( }} > - Reply in Thread + {threadActionLabel} )} @@ -1465,16 +1479,16 @@ export const Message = React.memo( > - {!isThreadedMessage && ( + {showThreadAction && ( onReplyClick(ev, true)} data-event-id={mEvent.getId()} variant="SurfaceVariant" size="300" radii="300" - aria-label="Reply in thread" + aria-label={threadReplies > 0 ? 'View thread' : 'Reply in thread'} > - + 0 ? Icons.Thread : Icons.ThreadPlus} size="100" /> )} {canEditEventOrCaption(mx, mEvent) && onEditId && (