From b52964dcb31886a1b9655a6cf860ebf65004c0f2 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Wed, 13 May 2026 14:30:52 -0400 Subject: [PATCH] fix(desktop): target replies from inbox threads Co-authored-by: Cursor --- desktop/src/features/home/lib/inbox.ts | 3 + desktop/src/features/home/ui/HomeView.tsx | 12 ++- .../src/features/home/ui/InboxDetailPane.tsx | 91 +++++++++++++++++-- 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/desktop/src/features/home/lib/inbox.ts b/desktop/src/features/home/lib/inbox.ts index 2bca6ffa88..6130c5ed28 100644 --- a/desktop/src/features/home/lib/inbox.ts +++ b/desktop/src/features/home/lib/inbox.ts @@ -39,8 +39,11 @@ export type InboxReply = { authorLabel: string; avatarUrl: string | null; content: string; + depth?: number; fullTimestampLabel: string; id: string; + parentId?: string | null; + rootId?: string | null; }; export type InboxContextMessage = InboxReply & { diff --git a/desktop/src/features/home/ui/HomeView.tsx b/desktop/src/features/home/ui/HomeView.tsx index a9509aa3df..1aa58c4e54 100644 --- a/desktop/src/features/home/ui/HomeView.tsx +++ b/desktop/src/features/home/ui/HomeView.tsx @@ -326,7 +326,12 @@ export function HomeView({ }); }} onOpenChannel={onOpenChannel} - onSendReply={async (content, mentionPubkeys, mediaTags) => { + onSendReply={async ({ + content, + mediaTags, + mentionPubkeys, + parentEventId, + }) => { const channelId = selectedItem?.item.channelId; if (!selectedItem || !channelId || !canReply) { throw new Error("Replies are not available for this item."); @@ -338,7 +343,7 @@ export function HomeView({ const result = await sendChannelMessage( channelId, content, - itemToReply.id, + parentEventId, mediaTags, mentionPubkeys, ); @@ -357,8 +362,11 @@ export function HomeView({ ?.avatarUrl ?? null) : null, content, + depth: result.depth, fullTimestampLabel: formatInboxFullTimestamp(result.createdAt), id: result.eventId, + parentId: result.parentEventId, + rootId: result.rootEventId, }; setLocalRepliesByItemId((current) => ({ ...current, diff --git a/desktop/src/features/home/ui/InboxDetailPane.tsx b/desktop/src/features/home/ui/InboxDetailPane.tsx index 65466f8683..9b3a3aac84 100644 --- a/desktop/src/features/home/ui/InboxDetailPane.tsx +++ b/desktop/src/features/home/ui/InboxDetailPane.tsx @@ -15,6 +15,8 @@ import type { InboxItem, InboxReply, } from "@/features/home/lib/inbox"; +import type { TimelineMessage } from "@/features/messages/types"; +import { MessageActionBar } from "@/features/messages/ui/MessageActionBar"; import { MessageComposer } from "@/features/messages/ui/MessageComposer"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; @@ -47,14 +49,32 @@ type InboxDetailPaneProps = { replies?: InboxReply[]; onDelete: () => void; onOpenChannel: (channelId: string) => void; - onSendReply: ( - content: string, - mentionPubkeys: string[], - mediaTags?: string[][], - ) => Promise; + onSendReply: (input: { + content: string; + mediaTags?: string[][]; + mentionPubkeys: string[]; + parentEventId: string; + }) => Promise; onToggleDone: () => void; }; +type InboxDisplayMessage = InboxContextMessage & { + depth: number; +}; + +function toActionBarMessage(message: InboxDisplayMessage): TimelineMessage { + return { + id: message.id, + author: message.authorLabel, + avatarUrl: message.avatarUrl, + body: message.content, + createdAt: 0, + depth: message.depth, + reactions: [], + time: message.fullTimestampLabel, + }; +} + export function InboxDetailPane({ canDelete, canOpenChannel, @@ -73,6 +93,8 @@ export function InboxDetailPane({ onToggleDone, }: InboxDetailPaneProps) { const detailPaneRef = React.useRef(null); + const [replyTargetId, setReplyTargetId] = React.useState(null); + const selectedItemId = item?.id ?? null; const focusComposer = React.useCallback(() => { window.requestAnimationFrame(() => { @@ -84,6 +106,11 @@ export function InboxDetailPane({ }); }, []); + React.useEffect(() => { + void selectedItemId; + setReplyTargetId(null); + }, [selectedItemId]); + if (!item) { return (
message.isSelected); - const pendingReplyMessages: InboxContextMessage[] = replies.map((reply) => ({ + const pendingReplyMessages: InboxDisplayMessage[] = replies.map((reply) => ({ ...reply, - depth: (selectedMessage?.depth ?? 0) + 1, + depth: reply.depth ?? (selectedMessage?.depth ?? 0) + 1, isSelected: false, mentionNames: [], })); - const displayMessages = + const displayMessages: InboxDisplayMessage[] = messages.length > 0 ? [...messages, ...pendingReplyMessages] : [ @@ -128,6 +155,24 @@ export function InboxDetailPane({ ...pendingReplyMessages, ]; const hasConversationContext = displayMessages.length > 1; + const replyTarget = + displayMessages.find((message) => message.id === replyTargetId) ?? null; + const composerParentEventId = replyTarget?.id ?? item.id; + const composerReplyTarget = + replyTarget && replyTarget.id !== item.id + ? { + author: replyTarget.authorLabel, + body: replyTarget.content, + id: replyTarget.id, + } + : null; + + const handleSelectReplyTarget = (message: InboxDisplayMessage) => { + setReplyTargetId((currentReplyTargetId) => + currentReplyTargetId === message.id ? null : message.id, + ); + focusComposer(); + }; return (
{ + setReplyTargetId(null); + focusComposer(); + }} icon={} /> @@ -282,6 +330,18 @@ export function InboxDetailPane({

{message.fullTimestampLabel}

+ {canReply ? ( +
+
+ handleSelectReplyTarget(message)} + reactions={[]} + /> +
+
+ ) : null}
setReplyTargetId(null) : undefined + } + onSend={(content, mentionPubkeys, mediaTags) => + onSendReply({ + content, + mediaTags, + mentionPubkeys, + parentEventId: composerParentEventId, + }) + } placeholder={ canReply ? `Send reply to ${item.channelLabel ? `#${item.channelLabel} thread` : "channel thread"}` : (disabledReplyReason ?? "Replies are not available for this item.") } + replyTarget={composerReplyTarget} />