Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ type ChannelPaneProps = {
onDelete?: (message: TimelineMessage) => void;
onEdit?: (message: TimelineMessage) => void;
onEditSave?: (content: string) => Promise<void>;
onExpandThreadReplies: (message: TimelineMessage) => void;
onJoinChannel?: () => Promise<void>;
onOpenAgentSession: (pubkey: string) => void;
onOpenDm?: (pubkeys: string[]) => void;
Expand Down Expand Up @@ -143,7 +142,6 @@ export const ChannelPane = React.memo(function ChannelPane({
onDelete,
onEdit,
onEditSave,
onExpandThreadReplies,
onJoinChannel,
onOpenAgentSession,
onOpenDm,
Expand Down Expand Up @@ -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}
Expand Down
10 changes: 1 addition & 9 deletions desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -322,7 +316,6 @@ export function ChannelScreen({
handleDelete,
handleEdit,
handleEditSave,
handleExpandThreadReplies,
handleOpenThread,
handleSendMessage,
handleSendThreadReply,
Expand Down Expand Up @@ -496,7 +489,6 @@ export function ChannelScreen({
onDelete={handleDelete}
onEdit={handleEdit}
onEditSave={handleEditSave}
onExpandThreadReplies={handleExpandThreadReplies}
onOpenAgentSession={handleOpenAgentSession}
onOpenDm={handleOpenDm}
onCloseProfilePanel={handleCloseProfilePanel}
Expand Down
68 changes: 67 additions & 1 deletion desktop/src/features/messages/lib/threadPanel.test.mjs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -56,3 +59,66 @@ test("buildMainTimelineEntries includes broadcast replies", () => {
["root", "broadcast-reply"],
);
});

test("buildThreadPanelData keeps direct replies flat", () => {
const root = message({ id: "root", createdAt: 1 });
const reply = message({
id: "reply",
createdAt: 2,
parentId: "root",
rootId: "root",
depth: 1,
tags: [["e", "root", "", "reply"]],
});

const panel = buildThreadPanelData([root, reply], "root", null, new Set());

assert.deepEqual(
panel.visibleReplies.map((entry) => ({
id: entry.message.id,
depth: entry.message.depth,
})),
[{ id: "reply", depth: 0 }],
);
});

test("buildThreadPanelData indents expanded subthread replies", () => {
const root = message({ id: "root", createdAt: 1 });
const reply = message({
id: "reply",
createdAt: 2,
parentId: "root",
rootId: "root",
depth: 1,
tags: [["e", "root", "", "reply"]],
});
const nestedReply = message({
id: "nested-reply",
createdAt: 3,
parentId: "reply",
rootId: "root",
depth: 2,
tags: [
["e", "root", "", "root"],
["e", "reply", "", "reply"],
],
});

const panel = buildThreadPanelData(
[root, reply, nestedReply],
"root",
null,
new Set(["reply"]),
);

assert.deepEqual(
panel.visibleReplies.map((entry) => ({
id: entry.message.id,
depth: entry.message.depth,
})),
[
{ id: "reply", depth: 0 },
{ id: "nested-reply", depth: 1 },
],
);
});
54 changes: 20 additions & 34 deletions desktop/src/features/messages/lib/threadPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,67 +150,56 @@ function buildSummaryForDirectReplies(
};
}

function appendExpandedReplies(params: {
function appendThreadPanelReplies(params: {
entries: MainTimelineEntry[];
parentId: string;
depth: number;
directChildrenByParentId: Map<string, TimelineMessage[]>;
descendantStatsByMessageId: Map<string, ThreadDescendantStats>;
expandedReplyIds: ReadonlySet<string>;
visitedMessageIds: Set<string>;
}) {
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<string, TimelineMessage[]>;
descendantStatsByMessageId: Map<string, ThreadDescendantStats>;
expandedReplyIds: ReadonlySet<string>;
}) {
const {
openThreadHeadId,
directChildrenByParentId,
descendantStatsByMessageId,
expandedReplyIds,
} = params;
const { openThreadHeadId, directChildrenByParentId } = params;
const entries: MainTimelineEntry[] = [];

appendExpandedReplies({
appendThreadPanelReplies({
entries,
parentId: openThreadHeadId,
depth: 1,
depth: 0,
directChildrenByParentId,
descendantStatsByMessageId,
expandedReplyIds,
visitedMessageIds: new Set([openThreadHeadId]),
});

return entries;
Expand Down Expand Up @@ -238,7 +227,6 @@ export function buildThreadPanelData(
messages: TimelineMessage[],
openThreadHeadId: string | null,
threadReplyTargetId: string | null,
expandedReplyIds: ReadonlySet<string>,
): ThreadPanelData {
if (!openThreadHeadId) {
return {
Expand Down Expand Up @@ -267,8 +255,6 @@ export function buildThreadPanelData(
const visibleReplies = buildVisibleThreadReplies({
openThreadHeadId,
directChildrenByParentId,
descendantStatsByMessageId,
expandedReplyIds,
});

const replyTargetInBranch =
Expand Down
6 changes: 4 additions & 2 deletions desktop/src/features/messages/ui/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export const MessageRow = React.memo(
<article
className={cn(
"group/message rounded-2xl px-2 py-1 transition-colors",
isThreadReplyLayout ? "space-y-1" : "flex items-start gap-2.5",
isThreadReplyLayout ? "space-y-0" : "flex items-start gap-2.5",
highlighted ? "bg-primary/10 ring-1 ring-primary/30" : "",
)}
data-message-id={message.id}
Expand Down Expand Up @@ -298,7 +298,9 @@ export const MessageRow = React.memo(
</div>
</div>
</div>
<div className="min-w-0 space-y-0.5">{messageBodyNode}</div>
<div className="-mt-0.5 min-w-0 space-y-0.5 pl-[26px]">
{messageBodyNode}
</div>
</>
) : (
<>
Expand Down
12 changes: 0 additions & 12 deletions desktop/src/features/messages/ui/MessageThreadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -35,7 +34,6 @@ type MessageThreadPanelProps = {
onDelete?: (message: TimelineMessage) => void;
onEdit?: (message: TimelineMessage) => void;
onEditSave?: (content: string) => Promise<void>;
onExpandReplies: (message: TimelineMessage) => void;
onResetWidth: () => void;
onResizeStart: (event: React.PointerEvent<HTMLButtonElement>) => void;
onScrollTargetResolved: () => void;
Expand Down Expand Up @@ -87,7 +85,6 @@ export function MessageThreadPanel({
onDelete,
onEdit,
onEditSave,
onExpandReplies,
onResetWidth,
onResizeStart,
onScrollTargetResolved,
Expand Down Expand Up @@ -244,15 +241,6 @@ export function MessageThreadPanel({
onToggleReaction={onToggleReaction}
profiles={profiles}
/>
{entry.summary ? (
<MessageThreadSummaryRow
depth={entry.message.depth}
layoutVariant="thread-reply"
message={entry.message}
onOpenThread={onExpandReplies}
summary={entry.summary}
/>
) : null}
</div>
);
})}
Expand Down
13 changes: 1 addition & 12 deletions desktop/tests/e2e/messaging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 ({
Expand Down
Loading