From 0fed686d435e011815fd112a43885893348fd174 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 11 May 2026 07:53:11 -0400 Subject: [PATCH] fix(desktop): show nested thread replies inline Co-authored-by: Cursor --- .../src/features/channels/ui/ChannelPane.tsx | 3 -- .../features/channels/ui/ChannelScreen.tsx | 10 +--- .../src/features/messages/lib/threadPanel.ts | 52 +++++++------------ .../messages/ui/MessageThreadPanel.tsx | 12 ----- desktop/tests/e2e/messaging.spec.ts | 13 +---- 5 files changed, 21 insertions(+), 69 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index c7b8b4bba9..2d2171ea79 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -81,7 +81,6 @@ type ChannelPaneProps = { onDelete?: (message: TimelineMessage) => void; onEdit?: (message: TimelineMessage) => void; onEditSave?: (content: string) => Promise; - onExpandThreadReplies: (message: TimelineMessage) => void; onJoinChannel?: () => Promise; onOpenAgentSession: (pubkey: string) => void; onOpenDm?: (pubkeys: string[]) => void; @@ -143,7 +142,6 @@ export const ChannelPane = React.memo(function ChannelPane({ onDelete, onEdit, onEditSave, - onExpandThreadReplies, onJoinChannel, onOpenAgentSession, onOpenDm, @@ -418,7 +416,6 @@ export const ChannelPane = React.memo(function ChannelPane({ onDelete={onDelete} onEdit={onEdit} onEditSave={onEditSave} - onExpandReplies={onExpandThreadReplies} onSelectReplyTarget={onSelectThreadReplyTarget} onSend={onSendThreadReply} onScrollTargetResolved={onThreadScrollTargetResolved} diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index b85a826ce4..9b8c294fe5 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -296,14 +296,8 @@ export function ChannelScreen({ timelineMessages, openThreadHeadId, threadReplyTargetId, - expandedThreadReplyIds, ), - [ - expandedThreadReplyIds, - openThreadHeadId, - threadReplyTargetId, - timelineMessages, - ], + [openThreadHeadId, threadReplyTargetId, timelineMessages], ); const openThreadHeadMessage = threadPanelData.threadHead; const threadMessages = threadPanelData.visibleReplies; @@ -322,7 +316,6 @@ export function ChannelScreen({ handleDelete, handleEdit, handleEditSave, - handleExpandThreadReplies, handleOpenThread, handleSendMessage, handleSendThreadReply, @@ -496,7 +489,6 @@ export function ChannelScreen({ onDelete={handleDelete} onEdit={handleEdit} onEditSave={handleEditSave} - onExpandThreadReplies={handleExpandThreadReplies} onOpenAgentSession={handleOpenAgentSession} onOpenDm={handleOpenDm} onCloseProfilePanel={handleCloseProfilePanel} diff --git a/desktop/src/features/messages/lib/threadPanel.ts b/desktop/src/features/messages/lib/threadPanel.ts index ad704f24cb..c2bd7c5425 100644 --- a/desktop/src/features/messages/lib/threadPanel.ts +++ b/desktop/src/features/messages/lib/threadPanel.ts @@ -150,67 +150,56 @@ function buildSummaryForDirectReplies( }; } -function appendExpandedReplies(params: { +function appendThreadPanelReplies(params: { entries: MainTimelineEntry[]; parentId: string; depth: number; directChildrenByParentId: Map; - descendantStatsByMessageId: Map; - expandedReplyIds: ReadonlySet; + visitedMessageIds: Set; }) { const { entries, parentId, depth, directChildrenByParentId, - descendantStatsByMessageId, - expandedReplyIds, + visitedMessageIds, } = params; const directReplies = directChildrenByParentId.get(parentId) ?? []; for (const reply of directReplies) { + if (visitedMessageIds.has(reply.id)) { + continue; + } + visitedMessageIds.add(reply.id); + entries.push({ message: normalizeInlineReplyMessage(reply, depth), - summary: buildSummaryForDirectReplies( - reply.id, - descendantStatsByMessageId, - ), + summary: null, }); - if (expandedReplyIds.has(reply.id)) { - appendExpandedReplies({ - entries, - parentId: reply.id, - depth: depth + 1, - directChildrenByParentId, - descendantStatsByMessageId, - expandedReplyIds, - }); - } + appendThreadPanelReplies({ + entries, + parentId: reply.id, + depth: depth + 1, + directChildrenByParentId, + visitedMessageIds, + }); } } function buildVisibleThreadReplies(params: { openThreadHeadId: string; directChildrenByParentId: Map; - descendantStatsByMessageId: Map; - expandedReplyIds: ReadonlySet; }) { - const { - openThreadHeadId, - directChildrenByParentId, - descendantStatsByMessageId, - expandedReplyIds, - } = params; + const { openThreadHeadId, directChildrenByParentId } = params; const entries: MainTimelineEntry[] = []; - appendExpandedReplies({ + appendThreadPanelReplies({ entries, parentId: openThreadHeadId, depth: 0, directChildrenByParentId, - descendantStatsByMessageId, - expandedReplyIds, + visitedMessageIds: new Set([openThreadHeadId]), }); return entries; @@ -238,7 +227,6 @@ export function buildThreadPanelData( messages: TimelineMessage[], openThreadHeadId: string | null, threadReplyTargetId: string | null, - expandedReplyIds: ReadonlySet, ): ThreadPanelData { if (!openThreadHeadId) { return { @@ -267,8 +255,6 @@ export function buildThreadPanelData( const visibleReplies = buildVisibleThreadReplies({ openThreadHeadId, directChildrenByParentId, - descendantStatsByMessageId, - expandedReplyIds, }); const replyTargetInBranch = diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index a12ad57cee..ea6ee338ac 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -16,7 +16,6 @@ import { } from "@/shared/ui/OverlayPanelBackdrop"; import { MessageComposer } from "./MessageComposer"; import { MessageRow } from "./MessageRow"; -import { MessageThreadSummaryRow } from "./MessageThreadSummaryRow"; import { TypingIndicatorRow } from "./TypingIndicatorRow"; import { useTimelineScrollManager } from "./useTimelineScrollManager"; @@ -35,7 +34,6 @@ type MessageThreadPanelProps = { onDelete?: (message: TimelineMessage) => void; onEdit?: (message: TimelineMessage) => void; onEditSave?: (content: string) => Promise; - onExpandReplies: (message: TimelineMessage) => void; onResetWidth: () => void; onResizeStart: (event: React.PointerEvent) => void; onScrollTargetResolved: () => void; @@ -87,7 +85,6 @@ export function MessageThreadPanel({ onDelete, onEdit, onEditSave, - onExpandReplies, onResetWidth, onResizeStart, onScrollTargetResolved, @@ -244,15 +241,6 @@ export function MessageThreadPanel({ onToggleReaction={onToggleReaction} profiles={profiles} /> - {entry.summary ? ( - - ) : null} ); })} diff --git a/desktop/tests/e2e/messaging.spec.ts b/desktop/tests/e2e/messaging.spec.ts index b645edb395..48f309acd7 100644 --- a/desktop/tests/e2e/messaging.spec.ts +++ b/desktop/tests/e2e/messaging.spec.ts @@ -424,8 +424,7 @@ test("opens a single-level thread panel with inline expansion", async ({ const firstReplySummaryRow = threadReplies.locator( `[data-thread-head-id="${firstReplyId}"]`, ); - await expect(firstReplySummaryRow).toHaveCount(1); - await expect(firstReplySummaryRow).toContainText("2 replies"); + await expect(firstReplySummaryRow).toHaveCount(0); await expect(rootSummaryRow).toContainText("18 replies"); await expect( @@ -448,16 +447,6 @@ test("opens a single-level thread panel with inline expansion", async ({ }); }) .toBeLessThanOrEqual(160); - - await firstReplySummaryRow.click(); - await expect( - threadReplies.getByTestId("message-row").filter({ hasText: nestedReply }), - ).toHaveCount(0); - await expect( - threadReplies - .getByTestId("message-row") - .filter({ hasText: nestedReplyFromBob }), - ).toHaveCount(0); }); test("thread panel width uses session storage and reset handle", async ({