mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
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:
@@ -822,6 +822,7 @@ export function HomeView({
|
||||
if (!selectedItem || !canDelete) return;
|
||||
void deleteInboxMessage(selectedItem.id);
|
||||
}}
|
||||
onDeleteMessage={deleteInboxMessage}
|
||||
onManageChannel={(channelId) => {
|
||||
handleCloseProfilePanel();
|
||||
setManagedChannelId(channelId);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
Reference in New Issue
Block a user