@bobfrankston/rmfmail 1.1.228 → 1.1.230

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
@@ -577,6 +577,29 @@ function refreshSyncTooltip(): void {
577
577
  // no new sync events.
578
578
  setInterval(refreshSyncTooltip, 30_000);
579
579
 
580
+ // Honest "Synced" indicator. "Synced HH:MM" must mean sync is IDLE, not fire
581
+ // on every tick — folderCountsChanged/syncProgress fire repeatedly during a
582
+ // catch-up backfill (e.g. after the daemon was offline) while messages are
583
+ // STILL arriving, so stamping "Synced" each time misled (Bob 2026-06-06: "it
584
+ // says synced but I don't see this afternoon's mail" — they were mid-backfill).
585
+ // Any sync activity (re)arms this settle timer; only when activity goes quiet
586
+ // for SYNC_SETTLE_MS do we stamp the time. While the storm runs, the handlers
587
+ // keep the bar on "Syncing…".
588
+ let syncSettleTimer: ReturnType<typeof setTimeout> | null = null;
589
+ const SYNC_SETTLE_MS = 4000;
590
+ function scheduleSyncedStamp(): void {
591
+ if (syncSettleTimer) clearTimeout(syncSettleTimer);
592
+ syncSettleTimer = setTimeout(() => {
593
+ syncSettleTimer = null;
594
+ const el = document.getElementById("status-sync");
595
+ // Only flip a "Syncing…" display to "Synced" — never clobber an error
596
+ // ("Sync error: …") or a transient action message ("Trashed 1 …").
597
+ if (el && (el.textContent || "").startsWith("Syncing")) {
598
+ el.textContent = `Synced ${new Date().toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false })}`;
599
+ }
600
+ }, SYNC_SETTLE_MS);
601
+ }
602
+
580
603
  // ── Auto two-line when message list is narrow ──
581
604
  const messageList = document.getElementById("message-list");
582
605
  if (messageList) {
@@ -1471,14 +1494,19 @@ async function deleteSelectedMessages(): Promise<void> {
1471
1494
  // An IPC 120s timeout doesn't mean the trash failed — surfacing it
1472
1495
  // as a status-bar error would only mislead. Real errors are still
1473
1496
  // reported by next sync's diagnostics.
1474
- const byAccount = new Map<string, number[]>();
1497
+ // Carry folderId per message so the daemon trashes the exact (folder, uid),
1498
+ // not whatever folder a bare (account, uid) lookup happens to resolve to
1499
+ // (Bob 2026-06-06: Del trashed the wrong message). uids/folderIds stay
1500
+ // index-aligned per account.
1501
+ const byAccount = new Map<string, { uids: number[]; folderIds: number[] }>();
1475
1502
  for (const msg of snapshot) {
1476
- const uids = byAccount.get(msg.accountId) || [];
1477
- uids.push(msg.uid);
1478
- byAccount.set(msg.accountId, uids);
1503
+ const g = byAccount.get(msg.accountId) || { uids: [], folderIds: [] };
1504
+ g.uids.push(msg.uid);
1505
+ g.folderIds.push(msg.folderId);
1506
+ byAccount.set(msg.accountId, g);
1479
1507
  }
1480
- for (const [accountId, uids] of byAccount) {
1481
- deleteMessages(accountId, uids).catch((e: any) => {
1508
+ for (const [accountId, g] of byAccount) {
1509
+ deleteMessages(accountId, g.uids, g.folderIds).catch((e: any) => {
1482
1510
  console.error(`Delete failed for ${accountId}: ${e?.message || e}`);
1483
1511
  if (statusSync) statusSync.textContent = `Delete sync issue (${accountId}): ${e?.message || e}`;
1484
1512
  });
@@ -2646,6 +2674,7 @@ onWsEvent((event) => {
2646
2674
  }
2647
2675
  if (statusSync) statusSync.textContent = `Syncing ${event.accountId}: ${label}`;
2648
2676
  if (startupStatus) startupStatus.textContent = `Syncing ${event.accountId}: ${label}`;
2677
+ scheduleSyncedStamp(); // re-arm the settle timer; stamp "Synced" when this quiets
2649
2678
  // Mark syncing folder in tree — bubble up to visible parent if collapsed
2650
2679
  const syncPath = event.phase?.startsWith("sync:") ? event.phase.slice(5) : null;
2651
2680
  // Clear previous syncing markers for this account
@@ -2672,6 +2701,7 @@ onWsEvent((event) => {
2672
2701
  refreshFolderTree();
2673
2702
  // Q53: track per-account last-sync timestamp for the status-bar hover.
2674
2703
  recordAccountSync(event.accountId);
2704
+ scheduleSyncedStamp(); // settle → "Synced HH:MM" once activity stops
2675
2705
  // Earlier I added reloadCurrentFolder() here to fix the
2676
2706
  // "phone INBOX stays on placeholder" report. That broke desktop
2677
2707
  // because syncComplete fires repeatedly on the desktop sync
@@ -2712,7 +2742,13 @@ onWsEvent((event) => {
2712
2742
  hideAlert();
2713
2743
  const syncBtn = document.getElementById("btn-sync") as HTMLButtonElement;
2714
2744
  if (syncBtn) { syncBtn.disabled = false; syncBtn.classList.remove("syncing"); }
2715
- if (statusSync) statusSync.textContent = `Synced ${new Date().toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false })}`;
2745
+ // Honest indicator: a count change means activity, not necessarily
2746
+ // "done". Show "Syncing…" and let the settle timer stamp the time
2747
+ // once the backfill goes quiet (see scheduleSyncedStamp).
2748
+ if (statusSync && !(statusSync.textContent || "").startsWith("Sync error")) {
2749
+ statusSync.textContent = "Syncing…";
2750
+ }
2751
+ scheduleSyncedStamp();
2716
2752
  break;
2717
2753
  }
2718
2754
  case "updateAvailable": {
@@ -1322,13 +1322,13 @@ function removeUserDictWord(word) {
1322
1322
  function flagSenderOrDomain(type, value) {
1323
1323
  return ipc().flagSenderOrDomain?.(type, value) ?? Promise.resolve({ flagged: false });
1324
1324
  }
1325
- function deleteMessage(accountId, uid) {
1326
- return ipc().deleteMessage?.(accountId, uid);
1325
+ function deleteMessage(accountId, uid, folderId) {
1326
+ return ipc().deleteMessage?.(accountId, uid, folderId);
1327
1327
  }
1328
- function deleteMessages(accountId, uids) {
1328
+ function deleteMessages(accountId, uids, folderIds) {
1329
1329
  if (uids.length === 1)
1330
- return deleteMessage(accountId, uids[0]);
1331
- return ipc().deleteMessages?.(accountId, uids);
1330
+ return deleteMessage(accountId, uids[0], folderIds?.[0]);
1331
+ return ipc().deleteMessages?.(accountId, uids, folderIds);
1332
1332
  }
1333
1333
  function moveMessages(accountId, uids, targetFolderId, targetAccountId) {
1334
1334
  if (uids.length === 1)