feat(desktop): add Inbox message delete action (#5779)

### What changed?

Inbox message action menus now show a standalone Delete action beside
Edit for manageable messages. Delete reuses the existing confirmation
and targets the message whose menu was opened, while the existing
empty-edit deletion path remains unchanged.

### Why?

Inbox users can delete a message directly without first entering edit
mode. Thread context can contain multiple messages, so the action must
preserve the active Inbox selection and delete only the chosen row.

### How is it tested?

Desktop checks, typechecking, builds, and test suites pass.

Added tests:

- [Inbox edit and delete E2E
coverage](https://github.com/block/buzz/tree/main/desktop/tests/e2e/inbox-edit.spec.ts)

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Tom Brow
2026-08-13 14:43:40 -07:00
committed by GitHub
parent bcf353c969
commit 514195b1d5
4 changed files with 90 additions and 2 deletions

View File

@@ -822,6 +822,7 @@ export function HomeView({
if (!selectedItem || !canDelete) return;
void deleteInboxMessage(selectedItem.id);
}}
onDeleteMessage={deleteInboxMessage}
onManageChannel={(channelId) => {
handleCloseProfilePanel();
setManagedChannelId(channelId);

View File

@@ -102,6 +102,7 @@ type InboxDetailPaneProps = {
latchedDefaultParentId?: string | null;
onBack?: () => void;
onDelete: () => void;
onDeleteMessage: (eventId: string) => void;
onEditTargetChange: React.Dispatch<React.SetStateAction<string | null>>;
onEditSave: (input: {
content: string;
@@ -170,6 +171,7 @@ function InboxMessageDetailPane({
latchedDefaultParentId = null,
onBack,
onDelete,
onDeleteMessage,
onEditTargetChange,
onEditSave,
onRequestEmptyEditDelete,
@@ -663,6 +665,11 @@ function InboxMessageDetailPane({
isFocusHighlightVisible={isFocusHighlightVisible}
key={message.id}
message={message}
onDelete={
canEditMessage
? () => onDeleteMessage(message.id)
: undefined
}
onEdit={canEditMessage ? handleSelectEditTarget : undefined}
onSelectReplyTarget={handleSelectReplyTarget}
onToggleReaction={onToggleReaction}

View File

@@ -32,6 +32,7 @@ type InboxMessageRowProps = {
isFirst?: boolean;
isFocusHighlightVisible: boolean;
message: InboxDisplayMessage;
onDelete?: (message: InboxDisplayMessage) => void;
onEdit?: (message: InboxDisplayMessage) => void;
onSelectReplyTarget: (message: InboxDisplayMessage) => void;
onToggleReaction?: (
@@ -50,6 +51,7 @@ export function InboxMessageRow({
isFirst = false,
isFocusHighlightVisible,
message,
onDelete,
onEdit,
onSelectReplyTarget,
onToggleReaction,
@@ -119,7 +121,7 @@ export function InboxMessageRow({
: "home-inbox-context-message"
}
>
{canReply || canToggleReactions || onEdit ? (
{canReply || canToggleReactions || onDelete || onEdit ? (
<div
className={cn(
"absolute right-2 top-1 z-10",
@@ -129,6 +131,7 @@ export function InboxMessageRow({
<MessageActionBar
channelId={channelId}
message={timelineMessage}
onDelete={onDelete ? () => onDelete(message) : undefined}
onEdit={onEdit ? () => onEdit(message) : undefined}
onReactionSelect={
canToggleReactions ? handleReactionSelect : undefined

View File

@@ -322,7 +322,7 @@ test("editing an immediate attachment reply preserves its media tags", async ({
).toHaveAttribute("href", ATTACHMENT_URL);
});
test("Inbox offers a working Edit action only for manageable messages", async ({
test("Inbox offers Edit and Delete actions only for manageable messages", async ({
page,
}) => {
await installMockBridge(page);
@@ -398,6 +398,9 @@ test("Inbox offers a working Edit action only for manageable messages", async ({
await expect(
page.getByTestId(`edit-message-${OWN_MESSAGE_ID}`),
).toBeVisible();
await expect(
page.getByTestId(`delete-message-${OWN_MESSAGE_ID}`),
).toBeVisible();
await waitForAnimations(page);
await page.screenshot({ path: `${SHOTS}/01-edit-action.png` });
await page.getByTestId(`edit-message-${OWN_MESSAGE_ID}`).click();
@@ -423,6 +426,9 @@ test("Inbox offers a working Edit action only for manageable messages", async ({
await expect(
page.getByTestId(`edit-message-${FOREIGN_MESSAGE_ID}`),
).toHaveCount(0);
await expect(
page.getByTestId(`delete-message-${FOREIGN_MESSAGE_ID}`),
).toHaveCount(0);
await page.keyboard.press("Escape");
await page.evaluate(async (channelId) => {
@@ -443,6 +449,77 @@ test("Inbox offers a working Edit action only for manageable messages", async ({
await expect(page.getByTestId(`edit-message-${OWN_MESSAGE_ID}`)).toHaveCount(
0,
);
await expect(
page.getByTestId(`delete-message-${OWN_MESSAGE_ID}`),
).toHaveCount(0);
});
test("cancelling explicit Inbox deletion preserves the message and selection", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/");
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
const { detail, rootRow } = await seedEmptyDeleteThread(page);
const selectedReplyRow = detail.locator(
`[data-message-id="${EMPTY_DELETE_REPLY_ID}"]`,
);
await openMoreActions(page, EMPTY_DELETE_ROOT_ID);
await page.getByTestId(`delete-message-${EMPTY_DELETE_ROOT_ID}`).click();
const dialog = page.getByRole("alertdialog");
await expect(dialog).toContainText("Delete message?");
await dialog.getByRole("button", { name: "Cancel" }).click();
await expect(dialog).toBeHidden();
await expect(rootRow).toContainText(EMPTY_DELETE_ROOT_CONTENT);
await expect(selectedReplyRow).toContainText(EMPTY_DELETE_REPLY_CONTENT);
await expect(selectedReplyRow).toHaveAttribute(
"data-testid",
"home-inbox-selected-message",
);
expect(
await page.evaluate(
() =>
((window as MockWindow).__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
(entry) => entry.command === "delete_message",
).length,
),
).toBe(0);
});
test("explicit Inbox deletion targets the chosen non-selected message", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/");
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
const { detail, rootRow } = await seedEmptyDeleteThread(page);
const selectedReplyRow = detail.locator(
`[data-message-id="${EMPTY_DELETE_REPLY_ID}"]`,
);
await openMoreActions(page, EMPTY_DELETE_ROOT_ID);
await page.getByTestId(`delete-message-${EMPTY_DELETE_ROOT_ID}`).click();
const dialog = page.getByRole("alertdialog");
await expect(dialog).toContainText("Delete message?");
await dialog.getByRole("button", { name: "Delete" }).click();
await expect(dialog).toBeHidden();
await expect(rootRow).toBeHidden();
await expect(selectedReplyRow).toContainText(EMPTY_DELETE_REPLY_CONTENT);
await expect(selectedReplyRow).toHaveAttribute(
"data-testid",
"home-inbox-selected-message",
);
const deletePayload = await page.evaluate(() => {
const payloads = (window as MockWindow).__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [];
return payloads.findLast((entry) => entry.command === "delete_message")
?.payload;
});
expect(deletePayload).toEqual(
expect.objectContaining({ eventId: EMPTY_DELETE_ROOT_ID }),
);
});
test("empty edit confirms deletion of the edited non-selected Inbox row", async ({