@bobfrankston/rmfmail 1.1.243 → 1.1.245

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/app.ts CHANGED
@@ -1418,7 +1418,7 @@ interface MovedBatch {
1418
1418
  // when older entries get shifted off the front of the stack).
1419
1419
  type UndoFlagEntry = { accountId: string; uid: number; prevFlagged: boolean };
1420
1420
  type UndoOp =
1421
- | { kind: "delete"; at: number; payload: DeletedMessage }
1421
+ | { kind: "delete"; at: number; payload: DeletedMessage[] }
1422
1422
  | { kind: "move"; at: number; payload: MovedBatch }
1423
1423
  | { kind: "flag"; at: number; payload: UndoFlagEntry[] };
1424
1424
 
@@ -1466,6 +1466,18 @@ async function deleteSelectedMessages(): Promise<void> {
1466
1466
  selected.push({ accountId: current.accountId, uid: current.message.uid, folderId: current.message.folderId });
1467
1467
  }
1468
1468
 
1469
+ // SAFETY GATE: confirm before a BULK delete. `Ctrl+A` selects every
1470
+ // visible row, and in the "All Inboxes" view that's a scattered screenful
1471
+ // across accounts — so a single Ctrl+A then Delete could silently trash
1472
+ // dozens of messages with no prompt. That is exactly what trashed 114 of
1473
+ // Bob's messages on 2026-06-12. A single-message delete (the common quick-
1474
+ // triage case) stays instant; anything larger must be confirmed.
1475
+ if (selected.length > 1) {
1476
+ if (!confirm(`Move ${selected.length} messages to Trash?\n\n(Ctrl+Z restores them if this was a mistake.)`)) {
1477
+ return;
1478
+ }
1479
+ }
1480
+
1469
1481
  const statusSync = document.getElementById("status-sync");
1470
1482
 
1471
1483
  // Optimistic UI: remove from list IMMEDIATELY, then queue the IPC.
@@ -1477,17 +1489,14 @@ async function deleteSelectedMessages(): Promise<void> {
1477
1489
  const snapshot = [...selected];
1478
1490
  removeMessagesAndReconcile(selected);
1479
1491
 
1480
- // Undo support set immediately too Ctrl+Z works the moment rows
1481
- // disappear from the list, not only after the daemon ACKs. Multi-select
1482
- // deletes only support undo for the first message today (consistent with
1483
- // the previous single-slot semantics); the loop+push pattern can be
1484
- // generalized later if Bob hits it.
1485
- if (snapshot.length === 1) {
1486
- pushUndo({ kind: "delete", at: Date.now(), payload: { ...snapshot[0], subject: "" } });
1487
- if (statusSync) statusSync.textContent = `Trashed 1 message (syncing) — Ctrl+Z to undo`;
1488
- } else {
1489
- if (statusSync) statusSync.textContent = `Trashed ${snapshot.length} messages (syncing)`;
1490
- }
1492
+ // Undo restores the WHOLE batch, not just the first message — a bulk
1493
+ // delete must be fully recoverable via Ctrl+Z (the old single-slot undo
1494
+ // left the other N-1 unrecoverable, which made an accidental mass-delete
1495
+ // unrecoverable; Bob 2026-06-12).
1496
+ pushUndo({ kind: "delete", at: Date.now(), payload: snapshot.map(m => ({ ...m, subject: "" })) });
1497
+ if (statusSync) statusSync.textContent = snapshot.length === 1
1498
+ ? `Trashed 1 message (syncing) Ctrl+Z to undo`
1499
+ : `Trashed ${snapshot.length} messages (syncing) — Ctrl+Z to undo`;
1491
1500
 
1492
1501
  // Fire-and-forget per local-first: optimistic remove above already
1493
1502
  // updated the UI; the daemon-side trash is sync DB + queued IMAP.
@@ -1519,9 +1528,16 @@ async function performUndo(): Promise<void> {
1519
1528
  const statusSync = document.getElementById("status-sync");
1520
1529
  try {
1521
1530
  if (op.kind === "delete") {
1522
- const { accountId, uid, folderId } = op.payload;
1523
- await undeleteMessage(accountId, uid, folderId);
1524
- if (statusSync) statusSync.textContent = "Message restored";
1531
+ // payload is the full batch (was a single object pre-2026-06-12;
1532
+ // tolerate both shapes so an in-flight undo from an old build
1533
+ // doesn't throw).
1534
+ const msgs = Array.isArray(op.payload) ? op.payload : [op.payload];
1535
+ for (const m of msgs) {
1536
+ await undeleteMessage(m.accountId, m.uid, m.folderId);
1537
+ }
1538
+ if (statusSync) statusSync.textContent = msgs.length === 1
1539
+ ? "Message restored"
1540
+ : `Restored ${msgs.length} messages`;
1525
1541
  } else if (op.kind === "move") {
1526
1542
  const { messages } = op.payload;
1527
1543
  const byDest = new Map<string, { accountId: string; folderId: number; uids: number[] }>();