mirror of
https://github.com/block/buzz.git
synced 2026-11-08 10:21:46 +00:00
fix(desktop): target replies from inbox threads
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 & {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void>;
|
||||
onSendReply: (input: {
|
||||
content: string;
|
||||
mediaTags?: string[][];
|
||||
mentionPubkeys: string[];
|
||||
parentEventId: string;
|
||||
}) => Promise<void>;
|
||||
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<HTMLElement | null>(null);
|
||||
const [replyTargetId, setReplyTargetId] = React.useState<string | null>(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 (
|
||||
<section
|
||||
@@ -105,13 +132,13 @@ export function InboxDetailPane({
|
||||
|
||||
const channelId = item.item.channelId;
|
||||
const selectedMessage = messages.find((message) => 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 (
|
||||
<section
|
||||
@@ -185,7 +230,10 @@ export function InboxDetailPane({
|
||||
<div className="flex items-center gap-0.5">
|
||||
<HeaderIconAction
|
||||
label="Reply"
|
||||
onClick={focusComposer}
|
||||
onClick={() => {
|
||||
setReplyTargetId(null);
|
||||
focusComposer();
|
||||
}}
|
||||
icon={<Reply className="h-4 w-4" />}
|
||||
/>
|
||||
</div>
|
||||
@@ -282,6 +330,18 @@ export function InboxDetailPane({
|
||||
<p className="ml-auto text-xs text-muted-foreground">
|
||||
{message.fullTimestampLabel}
|
||||
</p>
|
||||
{canReply ? (
|
||||
<div className="relative ml-1">
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2">
|
||||
<MessageActionBar
|
||||
activeReplyTargetId={replyTargetId}
|
||||
message={toActionBarMessage(message)}
|
||||
onReply={() => handleSelectReplyTarget(message)}
|
||||
reactions={[]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="-mt-0.5">
|
||||
<Markdown
|
||||
@@ -308,13 +368,24 @@ export function InboxDetailPane({
|
||||
disabled={!canReply}
|
||||
draftKey={`inbox-reply:${item.id}`}
|
||||
isSending={isSendingReply}
|
||||
onSend={onSendReply}
|
||||
onCancelReply={
|
||||
composerReplyTarget ? () => 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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user