From 120dbad1363048fe510515f1e17a6a56acaa8dfe Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Tue, 19 May 2026 18:00:38 -0400 Subject: [PATCH] Refine thread reply alignment Co-authored-by: Cursor --- .../messages/lib/threadPanel.test.mjs | 46 ++++++++- .../src/features/messages/lib/threadPanel.ts | 2 +- .../src/features/messages/ui/MessageRow.tsx | 93 +++++++++++-------- .../messages/ui/MessageThreadPanel.tsx | 8 +- .../messages/ui/MessageThreadSummaryRow.tsx | 26 ++++-- 5 files changed, 125 insertions(+), 50 deletions(-) diff --git a/desktop/src/features/messages/lib/threadPanel.test.mjs b/desktop/src/features/messages/lib/threadPanel.test.mjs index 3ddf745bb..5129ec871 100644 --- a/desktop/src/features/messages/lib/threadPanel.test.mjs +++ b/desktop/src/features/messages/lib/threadPanel.test.mjs @@ -1,7 +1,10 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { buildMainTimelineEntries } from "./threadPanel.ts"; +import { + buildMainTimelineEntries, + buildThreadPanelData, +} from "./threadPanel.ts"; function message(overrides) { return { @@ -56,3 +59,44 @@ test("buildMainTimelineEntries includes broadcast replies", () => { ["root", "broadcast-reply"], ); }); + +test("buildThreadPanelData keeps direct comments unindented", () => { + const root = message({ id: "root", createdAt: 1 }); + const directComment = message({ + id: "direct-comment", + createdAt: 2, + parentId: "root", + rootId: "root", + depth: 1, + tags: [["e", "root", "", "reply"]], + }); + const nestedReply = message({ + id: "nested-reply", + createdAt: 3, + parentId: "direct-comment", + rootId: "root", + depth: 2, + tags: [ + ["e", "root", "", "root"], + ["e", "direct-comment", "", "reply"], + ], + }); + + const panelData = buildThreadPanelData( + [root, directComment, nestedReply], + "root", + "root", + new Set(["direct-comment"]), + ); + + assert.deepEqual( + panelData.visibleReplies.map((entry) => ({ + id: entry.message.id, + depth: entry.message.depth, + })), + [ + { id: "direct-comment", depth: 0 }, + { id: "nested-reply", depth: 1 }, + ], + ); +}); diff --git a/desktop/src/features/messages/lib/threadPanel.ts b/desktop/src/features/messages/lib/threadPanel.ts index 4c30dd786..5d8d0818b 100644 --- a/desktop/src/features/messages/lib/threadPanel.ts +++ b/desktop/src/features/messages/lib/threadPanel.ts @@ -215,7 +215,7 @@ function buildVisibleThreadReplies(params: { appendExpandedReplies({ entries, parentId: openThreadHeadId, - depth: 1, + depth: 0, directChildrenByParentId, descendantStatsByMessageId, expandedReplyIds, diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index 4e873a49d..c83411543 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -22,6 +22,9 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; const DiffMessage = React.lazy(() => import("./DiffMessage")); const DiffMessageExpanded = React.lazy(() => import("./DiffMessageExpanded")); +const MESSAGE_TEXT_OFFSET_PX = 54; +const NESTED_REPLY_OFFSET_PX = 28; + export const MessageRow = React.memo( function MessageRow({ activeReplyTargetId = null, @@ -84,11 +87,21 @@ export const MessageRow = React.memo( ); const visibleDepth = Math.min(message.depth, 6); - const indentPx = visibleDepth * 28; + const indentPx = + visibleDepth > 0 + ? MESSAGE_TEXT_OFFSET_PX + (visibleDepth - 1) * NESTED_REPLY_OFFSET_PX + : 0; const depthGuideOffsets = React.useMemo(() => { - return Array.from( - { length: visibleDepth }, - (_, index) => 14 + index * 28, + if (visibleDepth === 0) { + return []; + } + + return Array.from({ length: visibleDepth }, (_, index) => + index === 0 + ? MESSAGE_TEXT_OFFSET_PX / 2 + : MESSAGE_TEXT_OFFSET_PX + + NESTED_REPLY_OFFSET_PX / 2 + + (index - 1) * NESTED_REPLY_OFFSET_PX, ); }, [visibleDepth]); const getTag = (name: string) => @@ -136,12 +149,8 @@ export const MessageRow = React.memo( const isThreadReplyLayout = layoutVariant === "thread-reply"; const guideBleedPx = isThreadReplyLayout ? 4 : 0; - const avatarSizeClass = isThreadReplyLayout - ? "!h-5 !w-5 !rounded-md" - : "!h-9 !w-9"; - const avatarButtonRadiusClass = isThreadReplyLayout - ? "rounded-md" - : "rounded-xl"; + const avatarSizeClass = "!h-9 !w-9"; + const avatarButtonRadiusClass = "rounded-xl"; const respondToDotColor = message.respondTo === "anyone" @@ -291,7 +300,7 @@ export const MessageRow = React.memo(
{isThreadReplyLayout ? ( <> -
- {message.pubkey ? ( - + - - ) : ( - <> -
- {avatarNode} -
- {authorNode} - - )} -
+ + + ) : ( + authorNode + )} {inlineMetadataNode} {message.personaDisplayName && message.personaDisplayName !== message.author ? ( @@ -334,8 +353,8 @@ export const MessageRow = React.memo( ) : null}
+
{messageBodyNode}
-
{messageBodyNode}
) : ( <> diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index b8ea0fdf3..4eb897816 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -228,10 +228,13 @@ export function MessageThreadPanel({ data-testid="message-thread-replies" > {threadReplies.length > 0 ? ( -
+
{threadReplies.map((entry) => { return ( -
+
void; summary: TimelineThreadSummary; }) { const visibleDepth = Math.min(Math.max(depth, 0), 6); - const messageTextOffsetPx = layoutVariant === "thread-reply" ? 8 : 50; - const marginLeftPx = visibleDepth * 28 + messageTextOffsetPx; - const depthGuideOffsets = Array.from( - { length: visibleDepth }, - (_, index) => 14 + index * 28, - ); + const indentPx = + visibleDepth > 0 + ? MESSAGE_TEXT_OFFSET_PX + (visibleDepth - 1) * NESTED_REPLY_OFFSET_PX + : 0; + const marginLeftPx = indentPx + MESSAGE_TEXT_OFFSET_PX; + const depthGuideOffsets = + visibleDepth === 0 + ? [] + : Array.from({ length: visibleDepth }, (_, index) => + index === 0 + ? MESSAGE_TEXT_OFFSET_PX / 2 + : MESSAGE_TEXT_OFFSET_PX + + NESTED_REPLY_OFFSET_PX / 2 + + (index - 1) * NESTED_REPLY_OFFSET_PX, + ); return (