@bobfrankston/rmfmail 1.2.49 → 1.2.65

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.
Files changed (90) hide show
  1. package/client/android-bootstrap.bundle.js +63 -2
  2. package/client/android-bootstrap.bundle.js.map +4 -4
  3. package/client/app.bundle.js +417 -63
  4. package/client/app.bundle.js.map +3 -3
  5. package/client/app.js +182 -24
  6. package/client/app.js.map +1 -1
  7. package/client/app.ts +166 -21
  8. package/client/components/calendar-sidebar.js +5 -5
  9. package/client/components/calendar-sidebar.js.map +1 -1
  10. package/client/components/calendar-sidebar.ts +5 -5
  11. package/client/components/folder-tree.js +81 -0
  12. package/client/components/folder-tree.js.map +1 -1
  13. package/client/components/folder-tree.ts +70 -0
  14. package/client/components/message-list.js +282 -38
  15. package/client/components/message-list.js.map +1 -1
  16. package/client/components/message-list.ts +264 -36
  17. package/client/compose/compose.bundle.js +70 -14
  18. package/client/compose/compose.bundle.js.map +2 -2
  19. package/client/compose/compose.css +28 -7
  20. package/client/compose/compose.js +64 -4
  21. package/client/compose/compose.js.map +1 -1
  22. package/client/compose/compose.ts +66 -4
  23. package/client/compose/editor.js +23 -7
  24. package/client/compose/editor.js.map +1 -1
  25. package/client/compose/editor.ts +23 -7
  26. package/client/help/search-help.js +4 -3
  27. package/client/help/search-help.js.map +1 -1
  28. package/client/help/search-help.ts +4 -3
  29. package/client/index.html +5 -1
  30. package/client/lib/api-client.js +4 -2
  31. package/client/lib/api-client.js.map +1 -1
  32. package/client/lib/api-client.ts +4 -2
  33. package/client/lib/mailxapi.js +2 -2
  34. package/client/lib/rmf-tiny.js +39 -6
  35. package/client/styles/components.css +6 -0
  36. package/docs/azure.md +58 -0
  37. package/docs/config-help.md +131 -0
  38. package/docs/edit-in-word-docx.md +98 -0
  39. package/docs/host-abstraction-plan.md +169 -0
  40. package/docs/local-first-plan.md +303 -0
  41. package/docs/npmglobalize-transitive-workspace-deps.md +107 -0
  42. package/docs/outlook.md +80 -18
  43. package/docs/outlook.txt +35 -0
  44. package/docs/rules-design.md +172 -0
  45. package/package.json +7 -7
  46. package/packages/mailx-imap/index.d.ts +48 -1
  47. package/packages/mailx-imap/index.d.ts.map +1 -1
  48. package/packages/mailx-imap/index.js +332 -70
  49. package/packages/mailx-imap/index.js.map +1 -1
  50. package/packages/mailx-imap/index.ts +323 -67
  51. package/packages/mailx-imap/microsoft-credentials.json +8 -0
  52. package/packages/mailx-imap/package-lock.json +2 -2
  53. package/packages/mailx-imap/package.json +1 -1
  54. package/packages/mailx-imap/providers/outlook-api.d.ts +7 -0
  55. package/packages/mailx-imap/providers/outlook-api.d.ts.map +1 -0
  56. package/packages/mailx-imap/providers/outlook-api.js +7 -0
  57. package/packages/mailx-imap/providers/outlook-api.js.map +1 -0
  58. package/packages/mailx-service/index.d.ts +7 -1
  59. package/packages/mailx-service/index.d.ts.map +1 -1
  60. package/packages/mailx-service/index.js +8 -8
  61. package/packages/mailx-service/index.js.map +1 -1
  62. package/packages/mailx-service/index.ts +8 -7
  63. package/packages/mailx-service/jsonrpc.js +1 -1
  64. package/packages/mailx-service/jsonrpc.js.map +1 -1
  65. package/packages/mailx-service/jsonrpc.ts +1 -1
  66. package/packages/mailx-settings/docs/azure.md +58 -0
  67. package/packages/mailx-settings/docs/config-help.md +131 -0
  68. package/packages/mailx-settings/docs/edit-in-word-docx.md +98 -0
  69. package/packages/mailx-settings/docs/host-abstraction-plan.md +169 -0
  70. package/packages/mailx-settings/docs/local-first-plan.md +303 -0
  71. package/packages/mailx-settings/docs/npmglobalize-transitive-workspace-deps.md +107 -0
  72. package/packages/mailx-settings/docs/outlook.md +80 -18
  73. package/packages/mailx-settings/docs/rules-design.md +172 -0
  74. package/packages/mailx-settings/index.d.ts.map +1 -1
  75. package/packages/mailx-settings/index.js +9 -8
  76. package/packages/mailx-settings/index.js.map +1 -1
  77. package/packages/mailx-settings/index.ts +9 -8
  78. package/packages/mailx-settings/package.json +1 -1
  79. package/packages/mailx-store/db.d.ts +50 -1
  80. package/packages/mailx-store/db.d.ts.map +1 -1
  81. package/packages/mailx-store/db.js +120 -6
  82. package/packages/mailx-store/db.js.map +1 -1
  83. package/packages/mailx-store/db.ts +126 -6
  84. package/packages/mailx-store/package.json +1 -1
  85. package/packages/mailx-types/contact-rules.d.ts +1 -1
  86. package/packages/mailx-types/contact-rules.js +1 -1
  87. package/packages/mailx-types/contact-rules.js.map +1 -1
  88. package/packages/mailx-types/contact-rules.jsonc +1 -1
  89. package/packages/mailx-types/contact-rules.ts +1 -1
  90. package/packages/mailx-types/package.json +1 -1
@@ -370,8 +370,8 @@ function markFolderRead(accountId, folderId) {
370
370
  function createFolder(accountId, parentPath, name) {
371
371
  return ipc().createFolder?.(accountId, parentPath, name);
372
372
  }
373
- function renameFolder(accountId, folderId, newName) {
374
- return ipc().renameFolder?.(accountId, folderId, newName);
373
+ function renameFolder(accountId, folderId, newName, newParentId) {
374
+ return ipc().renameFolder?.(accountId, folderId, newName, newParentId);
375
375
  }
376
376
  function deleteFolder(accountId, folderId) {
377
377
  return ipc().deleteFolder?.(accountId, folderId);
@@ -2760,10 +2760,12 @@ __export(message_list_exports, {
2760
2760
  releaseFocus: () => releaseFocus,
2761
2761
  reloadCurrentFolder: () => reloadCurrentFolder,
2762
2762
  removeMessagesAndReconcile: () => removeMessagesAndReconcile,
2763
+ revealMessage: () => revealMessage,
2763
2764
  scrollFocusedIntoView: () => scrollFocusedIntoView,
2764
2765
  setLiveFilter: () => setLiveFilter,
2765
2766
  setRowFlagged: () => setRowFlagged,
2766
2767
  setRowSeen: () => setRowSeen,
2768
+ showSelectionMenu: () => showSelectionMenu,
2767
2769
  showThreadPopup: () => showThreadPopup
2768
2770
  });
2769
2771
  function setLiveFilter(query) {
@@ -2784,6 +2786,12 @@ function applyLiveFilter(body) {
2784
2786
  row.classList.toggle("filter-hidden", !text.includes(liveFilterText));
2785
2787
  }
2786
2788
  }
2789
+ function emptyListMessage() {
2790
+ if (searchMode && currentSearchQuery) {
2791
+ return `No messages found by "${escapeHtmlText(currentSearchQuery)}"`;
2792
+ }
2793
+ return "No messages";
2794
+ }
2787
2795
  function cacheKey(mode, a, f, flagged, q) {
2788
2796
  if (mode === "folder")
2789
2797
  return `folder:${a}:${f}:${flagged ? "flag" : ""}`;
@@ -2961,6 +2969,154 @@ function setRowSeen(accountId, uid, yes) {
2961
2969
  if (row)
2962
2970
  row.setUnreadClass(!yes);
2963
2971
  }
2972
+ function selectedRows() {
2973
+ const out = [];
2974
+ for (const s of getSelectedMessages()) {
2975
+ const row = rowByKey.get(rowKey(s.accountId, s.uid));
2976
+ if (row)
2977
+ out.push(row);
2978
+ }
2979
+ return out;
2980
+ }
2981
+ function visibleRowObjs() {
2982
+ const body = document.getElementById("ml-body");
2983
+ if (!body)
2984
+ return [];
2985
+ return Array.from(body.querySelectorAll(".ml-row:not(.filter-hidden)")).map((el) => rowByKey.get(rowKey(el.dataset.accountId || "", Number(el.dataset.uid)))).filter((r) => !!r);
2986
+ }
2987
+ function bulkTargets() {
2988
+ const sel = selectedRows();
2989
+ return sel.length > 0 ? sel : visibleRowObjs();
2990
+ }
2991
+ async function bulkSetFlagged(rows, flagged) {
2992
+ const undo = [];
2993
+ await Promise.all(rows.map(async (row) => {
2994
+ const prev = row.isFlagged;
2995
+ if (prev === flagged)
2996
+ return;
2997
+ const prevFlags = [...row.msg.flags || []];
2998
+ row.isFlagged = flagged;
2999
+ try {
3000
+ await updateFlags(row.accountId, row.msg.uid, row.msg.flags);
3001
+ updateMessageFlags(row.accountId, row.msg.uid, row.msg.flags);
3002
+ undo.push({ accountId: row.accountId, uid: row.msg.uid, prevFlagged: prev });
3003
+ } catch {
3004
+ row.msg.flags = prevFlags;
3005
+ row.setFlaggedClass(prev);
3006
+ }
3007
+ }));
3008
+ if (undo.length)
3009
+ document.dispatchEvent(new CustomEvent("mailx-flagged-batch", { detail: undo }));
3010
+ }
3011
+ async function bulkSetSeen(rows, seen) {
3012
+ await Promise.all(rows.map(async (row) => {
3013
+ const prev = row.isSeen;
3014
+ if (prev === seen)
3015
+ return;
3016
+ const prevFlags = [...row.msg.flags || []];
3017
+ row.isSeen = seen;
3018
+ try {
3019
+ await updateFlags(row.accountId, row.msg.uid, row.msg.flags);
3020
+ updateMessageFlags(row.accountId, row.msg.uid, row.msg.flags);
3021
+ } catch {
3022
+ row.msg.flags = prevFlags;
3023
+ row.setUnreadClass(!prev);
3024
+ }
3025
+ }));
3026
+ }
3027
+ async function bulkMoveSelection() {
3028
+ const targets = bulkTargets();
3029
+ if (targets.length === 0)
3030
+ return;
3031
+ const accountId = targets[0].accountId;
3032
+ const same = targets.filter((r) => r.accountId === accountId);
3033
+ const pick = await pickFolder(accountId, { excludeFolderIds: [same[0].msg.folderId] });
3034
+ if (!pick)
3035
+ return;
3036
+ const uids = same.map((r) => r.msg.uid);
3037
+ removeMessagesAndReconcile(uids.map((u) => ({ accountId, uid: u })));
3038
+ const statusSync = document.getElementById("status-sync");
3039
+ if (statusSync)
3040
+ statusSync.textContent = `Moving ${uids.length} message${uids.length !== 1 ? "s" : ""} to ${pick.folderName}\u2026`;
3041
+ moveMessages(accountId, uids, pick.folderId).then(() => {
3042
+ if (statusSync)
3043
+ statusSync.textContent = `Moved ${uids.length} to ${pick.folderName}`;
3044
+ }).catch((err) => {
3045
+ if (statusSync)
3046
+ statusSync.textContent = `Move sync issue: ${err?.message || err}`;
3047
+ });
3048
+ exitMultiSelect();
3049
+ }
3050
+ async function showSelectionMenu(x, y) {
3051
+ const { showContextMenu: showContextMenu2 } = await Promise.resolve().then(() => (init_context_menu(), context_menu_exports));
3052
+ const body = document.getElementById("ml-body");
3053
+ const visibleRows = body ? Array.from(body.querySelectorAll(".ml-row:not(.filter-hidden)")) : [];
3054
+ const enterMulti = () => body?.classList.add("multi-select-on");
3055
+ const shownCount = visibleRows.length;
3056
+ const selCount = getSelectedMessages().length;
3057
+ const hasShown = shownCount > 0;
3058
+ const targetN = selCount > 0 ? selCount : shownCount;
3059
+ const onWhat = selCount > 0 ? `selected (${selCount})` : `all shown (${shownCount})`;
3060
+ const canAct = targetN > 0;
3061
+ const items = [
3062
+ { label: `Acting on ${onWhat}`, action: () => {
3063
+ }, disabled: true },
3064
+ { label: "", action: () => {
3065
+ }, separator: true },
3066
+ {
3067
+ label: `Select all shown (${shownCount})`,
3068
+ action: () => {
3069
+ if (!body)
3070
+ return;
3071
+ enterMulti();
3072
+ for (const r of visibleRows)
3073
+ r.classList.add("selected");
3074
+ lastClickedRow = visibleRows[visibleRows.length - 1] || null;
3075
+ updateBulkBar();
3076
+ },
3077
+ disabled: !hasShown
3078
+ },
3079
+ {
3080
+ label: "Select flagged \u2605 among shown",
3081
+ action: () => {
3082
+ if (!body)
3083
+ return;
3084
+ enterMulti();
3085
+ for (const r of visibleRows)
3086
+ r.classList.toggle("selected", r.classList.contains("flagged"));
3087
+ lastClickedRow = null;
3088
+ updateBulkBar();
3089
+ },
3090
+ disabled: !hasShown
3091
+ },
3092
+ {
3093
+ label: "Invert selection among shown",
3094
+ action: () => {
3095
+ if (!body)
3096
+ return;
3097
+ enterMulti();
3098
+ for (const r of visibleRows)
3099
+ r.classList.toggle("selected");
3100
+ updateBulkBar();
3101
+ },
3102
+ disabled: !hasShown
3103
+ },
3104
+ { label: `Deselect all (${selCount})`, action: () => exitMultiSelect(), disabled: selCount === 0 },
3105
+ { label: "", action: () => {
3106
+ }, separator: true },
3107
+ // Actions — auto-targeted (selected else all shown). No count on the
3108
+ // verb itself (a number on "Flag" reads as ambiguous, Bob 2026-06-24);
3109
+ // the "Acting on …" header above carries the target count.
3110
+ { label: "Flag", action: () => bulkSetFlagged(bulkTargets(), true), disabled: !canAct },
3111
+ { label: "Unflag", action: () => bulkSetFlagged(bulkTargets(), false), disabled: !canAct },
3112
+ { label: "Mark read", action: () => bulkSetSeen(bulkTargets(), true), disabled: !canAct },
3113
+ { label: "Mark unread", action: () => bulkSetSeen(bulkTargets(), false), disabled: !canAct },
3114
+ { label: "", action: () => {
3115
+ }, separator: true },
3116
+ { label: "Move to folder\u2026", action: () => bulkMoveSelection(), disabled: !canAct }
3117
+ ];
3118
+ showContextMenu2(x, y, items);
3119
+ }
2964
3120
  function scrollFocusedIntoView() {
2965
3121
  if (!focusedRow)
2966
3122
  return;
@@ -3216,7 +3372,7 @@ function removeMessagesAndReconcile(uids) {
3216
3372
  }
3217
3373
  }
3218
3374
  if (getMessages2().length === 0) {
3219
- body.innerHTML = `<div class="ml-empty">No messages</div>`;
3375
+ body.innerHTML = `<div class="ml-empty">${emptyListMessage()}</div>`;
3220
3376
  }
3221
3377
  }
3222
3378
  if (outcome.focusedWasRemoved) {
@@ -3273,10 +3429,15 @@ async function loadUnifiedInbox(autoSelect = true) {
3273
3429
  if (cached) {
3274
3430
  totalMessages = cached.total;
3275
3431
  setMessages(cached.items);
3276
- renderMessages(body, "", cached.items, { background: !autoSelect });
3432
+ const renderCached = () => renderMessages(body, "", cached.items, { background: !autoSelect });
3433
+ if (!autoSelect)
3434
+ withScrollAnchor(body, renderCached);
3435
+ else
3436
+ renderCached();
3277
3437
  const targetUuid = remembered ? pickRestoreUid(cached.items, remembered) : null;
3278
3438
  if (targetUuid) {
3279
- body.scrollTop = savedScroll;
3439
+ if (autoSelect)
3440
+ body.scrollTop = savedScroll;
3280
3441
  restoreSelection(body, targetUuid);
3281
3442
  } else if (autoSelect) {
3282
3443
  selectFirst(body);
@@ -3360,7 +3521,7 @@ async function loadSearchResults(query, scope = "all", accountId = "", folderId
3360
3521
  totalMessages = matches.length;
3361
3522
  setMessages(matches);
3362
3523
  if (matches.length === 0) {
3363
- body.innerHTML = `<div class="ml-empty">No regex matches</div>`;
3524
+ body.innerHTML = `<div class="ml-empty">No messages found by "${escapeHtmlText(query)}"</div>`;
3364
3525
  return;
3365
3526
  }
3366
3527
  body.innerHTML = "";
@@ -3380,7 +3541,7 @@ async function loadSearchResults(query, scope = "all", accountId = "", folderId
3380
3541
  const partialNote = partial ? `<div class="ml-search-warning">\u26A0 Searched ${foldersSearched} folder(s); ${foldersFailed} could not be searched (timeout / connection). Results may be incomplete \u2014 retry to search them again.</div>` : "";
3381
3542
  if (result.items.length === 0) {
3382
3543
  setMessages([]);
3383
- body.innerHTML = partialNote + `<div class="ml-empty">No results for "${query}"${partial ? " in the folders that responded" : ""}</div>`;
3544
+ body.innerHTML = partialNote + `<div class="ml-empty">No messages found by "${escapeHtmlText(query)}"${partial ? " in the folders that responded" : ""}</div>`;
3384
3545
  return;
3385
3546
  }
3386
3547
  setMessages(result.items);
@@ -3391,7 +3552,10 @@ async function loadSearchResults(query, scope = "all", accountId = "", folderId
3391
3552
  body.innerHTML = `<div class="ml-empty">Search error: ${e.message}</div>`;
3392
3553
  }
3393
3554
  }
3394
- async function loadMessages(accountId, folderId, page = 1, specialUse = "", autoSelect = true) {
3555
+ function revealMessage(accountId, folderId, uid, specialUse = "") {
3556
+ loadMessages(accountId, folderId, 1, specialUse, true, uid);
3557
+ }
3558
+ async function loadMessages(accountId, folderId, page = 1, specialUse = "", autoSelect = true, focusUid) {
3395
3559
  const myGen = ++loadGen;
3396
3560
  searchMode = false;
3397
3561
  unifiedMode = false;
@@ -3418,11 +3582,21 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
3418
3582
  if (cached) {
3419
3583
  totalMessages = cached.total;
3420
3584
  setMessages(cached.items);
3421
- renderMessages(body, accountId, cached.items, { background: !autoSelect });
3585
+ const renderCached = () => renderMessages(body, accountId, cached.items, { background: !autoSelect });
3586
+ if (!autoSelect)
3587
+ withScrollAnchor(body, renderCached);
3588
+ else
3589
+ renderCached();
3422
3590
  const targetUuid = remembered ? pickRestoreUid(cached.items, remembered) : null;
3423
- if (targetUuid) {
3591
+ if (focusUid != null) {
3424
3592
  requestAnimationFrame(() => {
3425
- body.scrollTop = savedScroll;
3593
+ if (!focusByIdentity(accountId, focusUid, { scroll: true }) && autoSelect)
3594
+ selectFirst(body);
3595
+ });
3596
+ } else if (targetUuid) {
3597
+ requestAnimationFrame(() => {
3598
+ if (autoSelect)
3599
+ body.scrollTop = savedScroll;
3426
3600
  restoreSelection(body, targetUuid);
3427
3601
  });
3428
3602
  } else if (autoSelect) {
@@ -3457,7 +3631,14 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
3457
3631
  }
3458
3632
  }
3459
3633
  const targetUuid = remembered ? pickRestoreUid(result.items, remembered) : null;
3460
- if (targetUuid) {
3634
+ if (focusUid != null) {
3635
+ requestAnimationFrame(() => {
3636
+ if (myGen !== loadGen)
3637
+ return;
3638
+ if (!focusByIdentity(accountId, focusUid, { scroll: true }) && autoSelect)
3639
+ selectFirst(body);
3640
+ });
3641
+ } else if (targetUuid) {
3461
3642
  requestAnimationFrame(() => {
3462
3643
  if (myGen !== loadGen)
3463
3644
  return;
@@ -3482,12 +3663,20 @@ async function loadMoreMessages() {
3482
3663
  return;
3483
3664
  loading = true;
3484
3665
  currentPage++;
3666
+ const t0 = Date.now();
3667
+ const heightBefore = body.scrollHeight;
3668
+ const rowsBefore = body.querySelectorAll(".ml-row").length;
3485
3669
  try {
3486
3670
  const flaggedOnly = body.classList.contains("flagged-only");
3487
3671
  const result = searchMode ? await searchMessages(currentSearchQuery, currentPage) : unifiedMode ? await getUnifiedInbox(currentPage, 50, flaggedOnly) : await getMessages(currentAccountId2, currentFolderId, currentPage, 50, flaggedOnly);
3488
3672
  const current = getMessages2();
3489
3673
  setMessages([...current, ...result.items]);
3490
3674
  appendMessages(body, unifiedMode ? "" : currentAccountId2, result.items);
3675
+ const rowsAfter = body.querySelectorAll(".ml-row").length;
3676
+ const heightAfter = body.scrollHeight;
3677
+ const elapsed = Date.now() - t0;
3678
+ const grew = heightAfter > heightBefore;
3679
+ console.log(` [ml-loadmore] page=${currentPage} got=${result.items?.length ?? 0} rows ${rowsBefore}->${rowsAfter} height ${heightBefore}->${heightAfter} grew=${grew} total=${totalMessages} ${elapsed}ms${grew ? "" : " \u26A0 NO GROWTH \u2014 scroll will appear stuck"}`);
3491
3680
  } catch (e) {
3492
3681
  console.error(`Load more error: ${e.message}`);
3493
3682
  } finally {
@@ -3495,12 +3684,18 @@ async function loadMoreMessages() {
3495
3684
  }
3496
3685
  }
3497
3686
  function renderMessages(body, accountId, items, opts = {}) {
3687
+ const preserveSel = body.classList.contains("multi-select-on") ? new Set(Array.from(body.querySelectorAll(".ml-row.selected")).map((r) => r.dataset.uuid || "").filter(Boolean)) : null;
3498
3688
  const fragment = document.createDocumentFragment();
3499
3689
  const tempDiv = document.createElement("div");
3500
3690
  appendMessages(tempDiv, accountId, items);
3501
3691
  while (tempDiv.firstChild)
3502
3692
  fragment.appendChild(tempDiv.firstChild);
3503
3693
  body.replaceChildren(fragment);
3694
+ if (preserveSel && preserveSel.size > 0) {
3695
+ for (const uuid of preserveSel) {
3696
+ body.querySelector(`.ml-row[data-uuid="${CSS.escape(uuid)}"]`)?.classList.add("selected");
3697
+ }
3698
+ }
3504
3699
  try {
3505
3700
  maybeShowFilterEmptyState(body);
3506
3701
  } catch {
@@ -4002,6 +4197,14 @@ var init_message_list = __esm({
4002
4197
  }
4003
4198
  onFlagClick(e) {
4004
4199
  e.stopPropagation();
4200
+ const body = this.el.parentElement;
4201
+ if (body?.classList.contains("multi-select-on") && this.el.classList.contains("selected")) {
4202
+ const sel = selectedRows();
4203
+ if (sel.length > 1) {
4204
+ void bulkSetFlagged(sel, !this.el.classList.contains("flagged"));
4205
+ return;
4206
+ }
4207
+ }
4005
4208
  const newFlaggedState = !this.el.classList.contains("flagged");
4006
4209
  const prevFlags = [...this.msg.flags || []];
4007
4210
  this.isFlagged = newFlaggedState;
@@ -4139,33 +4342,23 @@ var init_message_list = __esm({
4139
4342
  const accountId = this.accountId;
4140
4343
  const msg = this.msg;
4141
4344
  const self = this;
4345
+ const selCount = getSelectedMessages().length;
4346
+ const countSfx = selCount > 1 ? ` (${selCount})` : "";
4347
+ const rowTargets = () => {
4348
+ const s = selectedRows();
4349
+ return s.length > 0 ? s : [self];
4350
+ };
4142
4351
  const items = [
4143
4352
  {
4144
- label: isSeen ? "Mark unread" : "Mark read",
4145
- action: async () => {
4146
- const probe = { flags: [...msg.flags || []] };
4147
- setSeen(probe, !isSeen);
4148
- try {
4149
- await updateFlags(accountId, msg.uid, probe.flags);
4150
- msg.flags = probe.flags;
4151
- updateMessageFlags(accountId, msg.uid, probe.flags);
4152
- self.setUnreadClass(isSeen);
4153
- } catch {
4154
- }
4155
- }
4353
+ // Target state = inverse of what the menu showed, matching what
4354
+ // the user clicked. Visual-state-based labels stay coherent with
4355
+ // the auto-mark-as-read debounce. Applies to the whole selection.
4356
+ label: (isSeen ? "Mark unread" : "Mark read") + countSfx,
4357
+ action: () => bulkSetSeen(rowTargets(), !isSeen)
4156
4358
  },
4157
4359
  {
4158
- label: isFlagged ? "Unflag" : "Flag",
4159
- action: async () => {
4160
- const probe = { flags: [...msg.flags || []] };
4161
- setFlagged(probe, !isFlagged);
4162
- try {
4163
- await updateFlags(accountId, msg.uid, probe.flags);
4164
- msg.flags = probe.flags;
4165
- self.setFlaggedClass(!isFlagged);
4166
- } catch {
4167
- }
4168
- }
4360
+ label: (isFlagged ? "Unflag" : "Flag") + countSfx,
4361
+ action: () => bulkSetFlagged(rowTargets(), !isFlagged)
4169
4362
  },
4170
4363
  { label: "", action: () => {
4171
4364
  }, separator: true },
@@ -4199,8 +4392,8 @@ var init_message_list = __esm({
4199
4392
  {
4200
4393
  label: "Move to folder\u2026",
4201
4394
  action: async () => {
4202
- const selectedRows = Array.from(document.querySelectorAll(".ml-row.selected"));
4203
- const uids = selectedRows.length > 0 ? selectedRows.map((r) => Number(r.dataset.uid)).filter((u) => !isNaN(u)) : [msg.uid];
4395
+ const selectedRows2 = Array.from(document.querySelectorAll(".ml-row.selected"));
4396
+ const uids = selectedRows2.length > 0 ? selectedRows2.map((r) => Number(r.dataset.uid)).filter((u) => !isNaN(u)) : [msg.uid];
4204
4397
  const pick = await pickFolder(accountId, { excludeFolderIds: [msg.folderId] });
4205
4398
  if (!pick)
4206
4399
  return;
@@ -4582,7 +4775,7 @@ function overdueTaskRowHtml(t) {
4582
4775
  }
4583
4776
  return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml4(t.uuid)}">
4584
4777
  <input type="checkbox" class="cal-side-overdue-check" title="Mark done">
4585
- <span class="cal-side-task-title">${escapeHtml4(t.title)}</span>
4778
+ <span class="cal-side-task-title" title="${escapeHtml4(t.title)}">${escapeHtml4(t.title)}</span>
4586
4779
  <span class="cal-side-task-due overdue">${escapeHtml4(dueLabel)}</span>
4587
4780
  </div>`;
4588
4781
  }
@@ -4643,7 +4836,7 @@ function renderEvents(events) {
4643
4836
  html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml4(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
4644
4837
  ${calIconHtml(calInfoFor(e.calendarId))}
4645
4838
  <span class="cal-side-event-time">${escapeHtml4(formatTime(e))}</span>
4646
- <span class="cal-side-event-title">${escapeHtml4(e.title)}<span class="cal-side-event-recur" title="Daily">\u21BB</span></span>
4839
+ <span class="cal-side-event-title" title="${escapeHtml4(e.title)}">${escapeHtml4(e.title)}<span class="cal-side-event-recur" title="Daily">\u21BB</span></span>
4647
4840
  </div>`;
4648
4841
  }
4649
4842
  }
@@ -4669,14 +4862,14 @@ function renderEvents(events) {
4669
4862
  const recurAttr = e.recurringEventId ? ' data-recurring="1"' : "";
4670
4863
  if (isHolidayKind || kind === "birthday") {
4671
4864
  html += `<div class="cal-side-event" data-holiday="1" data-holiday-kind="${kind}" data-id="${e.id}">
4672
- <span class="cal-side-event-title cal-side-event-holiday-title">${calIconHtml(info)} ${escapeHtml4(e.title)}</span>
4865
+ <span class="cal-side-event-title cal-side-event-holiday-title" title="${escapeHtml4(e.title)}">${calIconHtml(info)} ${escapeHtml4(e.title)}</span>
4673
4866
  </div>`;
4674
4867
  } else {
4675
4868
  const titleAttr = link ? 'title="Click to open in Google Calendar"' : "";
4676
4869
  html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml4(link)}" ${titleAttr}>
4677
4870
  ${calIconHtml(info)}
4678
4871
  <span class="cal-side-event-time">${escapeHtml4(formatTime(e))}</span>
4679
- <span class="cal-side-event-title">${escapeHtml4(e.title)}${recurMark}</span>
4872
+ <span class="cal-side-event-title" title="${escapeHtml4(e.title)}">${escapeHtml4(e.title)}${recurMark}</span>
4680
4873
  </div>`;
4681
4874
  }
4682
4875
  }
@@ -4761,7 +4954,7 @@ async function renderTasks(prefetched) {
4761
4954
  const sel = selectedTaskUuids.has(t.uuid) ? " selected" : "";
4762
4955
  html += `<div class="cal-side-task${sel}" data-uuid="${t.uuid}">
4763
4956
  <input type="checkbox" ${done ? "checked" : ""} class="cal-side-task-check">
4764
- <span class="cal-side-task-title${done ? " done" : ""}">${escapeHtml4(t.title)}</span>
4957
+ <span class="cal-side-task-title${done ? " done" : ""}" title="${escapeHtml4(t.title)}">${escapeHtml4(t.title)}</span>
4765
4958
  ${dueHtml}
4766
4959
  <button class="cal-side-task-delete" title="Delete task" aria-label="Delete task">\xD7</button>
4767
4960
  </div>`;
@@ -5920,12 +6113,13 @@ decides which operators work.</p>
5920
6113
  <h3>Qualifiers \u2014 work in every mode</h3>
5921
6114
  <table>
5922
6115
  <tr><th>Form</th><th>Effect</th></tr>
5923
- <tr><td><code>from:bob</code></td><td>Sender contains</td></tr>
5924
- <tr><td><code>to:eleanor</code></td><td>Recipient contains</td></tr>
6116
+ <tr><td><code>from:bob</code></td><td>Sender (name or address) contains</td></tr>
6117
+ <tr><td><code>to:eleanor</code></td><td>Recipient contains \u2014 matches <strong>To <em>or</em> Cc</strong> (anyone the message was addressed to)</td></tr>
6118
+ <tr><td><code>cc:dana</code></td><td>Cc only \u2014 when you want to exclude the main To line</td></tr>
5925
6119
  <tr><td><code>subject:lunch</code></td><td>Subject contains</td></tr>
5926
6120
  <tr><td><code>date:2026-05-01</code></td><td>On that date \u2014 also <code>date:&gt;1w</code>, <code>date:&lt;=2026-01-15</code></td></tr>
5927
6121
  <tr><td><code>after:1w</code> / <code>before:1m</code></td><td>Newer / older than \u2014 <code>d</code>, <code>w</code>, <code>m</code>, <code>y</code>, a date, <code>today</code>, <code>yesterday</code></td></tr>
5928
- <tr><td><code>has:attachment</code></td><td>Has an attachment</td></tr>
6122
+ <tr><td><code>has:attachment</code></td><td>Only messages that carry a file attachment</td></tr>
5929
6123
  <tr><td><code>is:unread</code></td><td>Also <code>is:flagged</code>, <code>is:read</code>, <code>is:answered</code>, <code>is:draft</code></td></tr>
5930
6124
  <tr><td><code>folder:sent</code></td><td>Restrict to folders whose name contains the term</td></tr>
5931
6125
  <tr><td><code>/regex/</code></td><td>Client-side regex over the currently-visible rows. Local only \u2014 never sent to the server.</td></tr>
@@ -6252,6 +6446,22 @@ function renderNode(node, container, depth) {
6252
6446
  folderEl.dataset.folderPath = node.path;
6253
6447
  folderEl.dataset.specialUse = node.specialUse || "";
6254
6448
  folderEl.style.paddingLeft = `${depth * 16 + 8}px`;
6449
+ if (node.id !== -1 && !node.specialUse) {
6450
+ folderEl.draggable = true;
6451
+ folderEl.addEventListener("dragstart", (e) => {
6452
+ e.dataTransfer.effectAllowed = "move";
6453
+ e.dataTransfer.setData("application/x-mailx-folder", JSON.stringify({
6454
+ accountId: node.accountId,
6455
+ folderId: node.id,
6456
+ name: node.name,
6457
+ path: node.path,
6458
+ // Carry the real hierarchy delimiter so the descendant-drop
6459
+ // guard doesn't have to guess it from the path (a top-level
6460
+ // folder like "Work" has neither "/" nor "." to infer from).
6461
+ delimiter: node.delimiter
6462
+ }));
6463
+ });
6464
+ }
6255
6465
  const toggle = document.createElement("span");
6256
6466
  toggle.className = "ft-toggle";
6257
6467
  if (hasChildren) {
@@ -6479,6 +6689,48 @@ function renderNode(node, container, depth) {
6479
6689
  clearTimeout(dragExpandTimer);
6480
6690
  dragExpandTimer = null;
6481
6691
  }
6692
+ const folderData = e.dataTransfer.getData("application/x-mailx-folder");
6693
+ if (folderData) {
6694
+ let src;
6695
+ try {
6696
+ src = JSON.parse(folderData);
6697
+ } catch {
6698
+ return;
6699
+ }
6700
+ const delim = src.delimiter || (node.path.includes("/") ? "/" : node.path.includes(".") ? "." : "/");
6701
+ if (src.accountId !== node.accountId) {
6702
+ const statusEl3 = document.getElementById("status-sync");
6703
+ if (statusEl3)
6704
+ statusEl3.textContent = "Folders can only be reparented within the same account.";
6705
+ return;
6706
+ }
6707
+ if (src.folderId === node.id)
6708
+ return;
6709
+ if (node.path === src.path || node.path.startsWith(src.path + delim)) {
6710
+ const statusEl3 = document.getElementById("status-sync");
6711
+ if (statusEl3)
6712
+ statusEl3.textContent = "Can't move a folder into one of its own subfolders.";
6713
+ return;
6714
+ }
6715
+ if (!confirm(`Move folder "${src.name}" into "${node.name}"?`))
6716
+ return;
6717
+ const statusEl2 = document.getElementById("status-sync");
6718
+ try {
6719
+ await renameFolder(src.accountId, src.folderId, src.name, node.id);
6720
+ if (statusEl2)
6721
+ statusEl2.textContent = `Moved "${src.name}" into "${node.name}"`;
6722
+ expandState[`${node.accountId}:${node.path}`] = true;
6723
+ saveExpandState();
6724
+ const treeContainer2 = document.getElementById("folder-tree");
6725
+ if (treeContainer2)
6726
+ loadFolderTree(treeContainer2);
6727
+ } catch (err) {
6728
+ if (statusEl2)
6729
+ statusEl2.textContent = `Move failed: ${err?.message || err}`;
6730
+ alert(`Failed: ${err?.message || err}`);
6731
+ }
6732
+ return;
6733
+ }
6482
6734
  const multiData = e.dataTransfer.getData("application/x-mailx-messages");
6483
6735
  const singleData = e.dataTransfer.getData("application/x-mailx-message");
6484
6736
  const messages2 = multiData ? JSON.parse(multiData) : singleData ? [JSON.parse(singleData)] : [];
@@ -7143,6 +7395,21 @@ init_message_viewer();
7143
7395
  init_api_client();
7144
7396
  init_message_state();
7145
7397
  installConsoleCapture();
7398
+ (function installStallWatchdog() {
7399
+ const EXPECTED_MS = 1e3;
7400
+ const STALL_MS = 250;
7401
+ let last = performance.now();
7402
+ const tick = () => {
7403
+ const now = performance.now();
7404
+ const slip = now - last - EXPECTED_MS;
7405
+ if (slip > STALL_MS) {
7406
+ console.warn(` [stall] main thread blocked ~${slip.toFixed(0)}ms (event loop frozen \u2014 UI was unresponsive this long)`);
7407
+ }
7408
+ last = now;
7409
+ setTimeout(tick, EXPECTED_MS);
7410
+ };
7411
+ setTimeout(tick, EXPECTED_MS);
7412
+ })();
7146
7413
  var APP_NAME = "rmfmail";
7147
7414
  function propagateAppName() {
7148
7415
  const set = (id, fn) => {
@@ -7727,6 +7994,11 @@ document.getElementById("btn-factory-reset")?.addEventListener("click", async ()
7727
7994
  });
7728
7995
  async function openCompose(mode, overrideMsg, overrideAccountId) {
7729
7996
  logClientEvent("openCompose-entry", { mode });
7997
+ for (const id of ["settings-dropdown", "view-dropdown", "restart-dropdown"]) {
7998
+ const dd = document.getElementById(id);
7999
+ if (dd && !dd.hidden) hideDropdownHard(dd);
8000
+ }
8001
+ refreshToolbarOverlayShield();
7730
8002
  const current = overrideMsg ? { message: overrideMsg, accountId: overrideAccountId || currentAccountId3 } : getCurrentMessage();
7731
8003
  if ((mode === "reply" || mode === "replyAll" || mode === "forward" || mode === "editAsNew") && !current) {
7732
8004
  console.warn(`[compose] ${mode} \u2014 no message selected`);
@@ -7738,7 +8010,7 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
7738
8010
  const titleSubject = mode === "new" ? "" : msg?.subject || "";
7739
8011
  const frame = showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
7740
8012
  const accounts = await accountsP;
7741
- const accountId = current?.accountId || accounts[0]?.id || "";
8013
+ const accountId = mode === "new" ? currentAccountId3 || current?.accountId || accounts[0]?.id || "" : current?.accountId || currentAccountId3 || accounts[0]?.id || "";
7742
8014
  const rePrefix = /^(re|fwd?):\s*/i;
7743
8015
  const cleanSubject = msg ? msg.subject.replace(rePrefix, "") : "";
7744
8016
  const init = {
@@ -7848,6 +8120,25 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
7848
8120
  init.subject = msg.subject || "";
7849
8121
  init.bodyHtml = editAsNewBody(msg);
7850
8122
  }
8123
+ if (msg && (mode === "editAsNew" || mode === "forward") && Array.isArray(msg.attachments) && msg.attachments.length) {
8124
+ const msgAccountId = current?.accountId || accountId;
8125
+ const loaded = [];
8126
+ for (const att of msg.attachments) {
8127
+ try {
8128
+ const data = await getAttachment(msgAccountId, msg.uid, att.id, msg.folderId);
8129
+ if (data?.content) {
8130
+ loaded.push({
8131
+ filename: data.filename || att.filename || "attachment",
8132
+ mimeType: data.contentType || att.contentType || "application/octet-stream",
8133
+ dataBase64: data.content
8134
+ });
8135
+ }
8136
+ } catch (e) {
8137
+ console.error(`[compose] ${mode}: failed to load attachment ${att.id}: ${e?.message || e}`);
8138
+ }
8139
+ }
8140
+ if (loaded.length) init.attachments = loaded;
8141
+ }
7851
8142
  const initJson = JSON.stringify(init);
7852
8143
  try {
7853
8144
  sessionStorage.setItem("composeInit", initJson);
@@ -8214,6 +8505,12 @@ async function performUndo() {
8214
8505
  document.addEventListener("mailx-moved", (e) => {
8215
8506
  pushUndo({ kind: "move", at: Date.now(), payload: e.detail });
8216
8507
  });
8508
+ document.addEventListener("mailx-flagged-batch", (e) => {
8509
+ const payload = e.detail;
8510
+ if (Array.isArray(payload) && payload.length) {
8511
+ pushUndo({ kind: "flag", at: Date.now(), payload });
8512
+ }
8513
+ });
8217
8514
  (() => {
8218
8515
  const SLOP = 6;
8219
8516
  let cand = null;
@@ -8486,6 +8783,11 @@ document.getElementById("btn-spam-report")?.addEventListener("click", async () =
8486
8783
  }
8487
8784
  });
8488
8785
  document.getElementById("btn-compose")?.addEventListener("click", () => openCompose("new"));
8786
+ document.getElementById("btn-selection")?.addEventListener("click", (e) => {
8787
+ e.stopPropagation();
8788
+ const r = e.currentTarget.getBoundingClientRect();
8789
+ void showSelectionMenu(r.left, r.bottom + 2);
8790
+ });
8489
8791
  document.getElementById("btn-mark-unread")?.addEventListener("click", () => {
8490
8792
  const sel = getCurrentFocused();
8491
8793
  if (!sel) return;
@@ -8841,6 +9143,20 @@ var currentAccountId3 = "";
8841
9143
  var currentFolderId2 = 0;
8842
9144
  var reloadDebounceTimer = null;
8843
9145
  var lastListReloadAt = 0;
9146
+ function closeSearchAndReveal() {
9147
+ const focused = getCurrentFocused();
9148
+ clearSearchMode();
9149
+ const body = document.getElementById("ml-body");
9150
+ if (body) body.querySelectorAll(".filter-hidden").forEach((r) => r.classList.remove("filter-hidden"));
9151
+ if (focused && focused.folderId) {
9152
+ currentAccountId3 = focused.accountId;
9153
+ currentFolderId2 = focused.folderId;
9154
+ revealMessage(focused.accountId, focused.folderId, focused.uid);
9155
+ } else {
9156
+ reloadCurrentFolder();
9157
+ }
9158
+ setTitle(APP_NAME);
9159
+ }
8844
9160
  searchInput?.addEventListener("input", () => {
8845
9161
  clearTimeout(searchTimeout);
8846
9162
  if (serverSearchTimer) {
@@ -8850,11 +9166,7 @@ searchInput?.addEventListener("input", () => {
8850
9166
  }
8851
9167
  updateSearchHighlight();
8852
9168
  if (searchInput.value.trim() === "") {
8853
- clearSearchMode();
8854
- const body = document.getElementById("ml-body");
8855
- if (body) body.querySelectorAll(".filter-hidden").forEach((r) => r.classList.remove("filter-hidden"));
8856
- reloadCurrentFolder();
8857
- setTitle(APP_NAME);
9169
+ closeSearchAndReveal();
8858
9170
  } else {
8859
9171
  searchTimeout = setTimeout(() => doSearch(false), 180);
8860
9172
  }
@@ -8872,11 +9184,7 @@ searchInput?.addEventListener("keydown", (e) => {
8872
9184
  }
8873
9185
  cancelServerSearch();
8874
9186
  updateSearchHighlight();
8875
- clearSearchMode();
8876
- const body = document.getElementById("ml-body");
8877
- if (body) body.querySelectorAll(".filter-hidden").forEach((r) => r.classList.remove("filter-hidden"));
8878
- reloadCurrentFolder();
8879
- setTitle(APP_NAME);
9187
+ closeSearchAndReveal();
8880
9188
  }
8881
9189
  });
8882
9190
  function rerunActiveSearch() {
@@ -9891,6 +10199,7 @@ var viewDropdown = document.getElementById("view-dropdown");
9891
10199
  var optTwoLine = document.getElementById("opt-two-line");
9892
10200
  var optPreview = document.getElementById("opt-preview");
9893
10201
  var optSnippet = document.getElementById("opt-snippet");
10202
+ var optFlagStars = document.getElementById("opt-flag-stars");
9894
10203
  var optThreaded = document.getElementById("opt-threaded");
9895
10204
  var optFlagged = document.getElementById("opt-flagged");
9896
10205
  var optFolderCounts = document.getElementById("opt-folder-counts");
@@ -9925,6 +10234,13 @@ function hideDropdownHard(dd) {
9925
10234
  dd.hidden = true;
9926
10235
  dd.style.cssText = "";
9927
10236
  }
10237
+ var tbEscapeCleanup = null;
10238
+ function closeToolbarDropdowns() {
10239
+ for (const id of ["settings-dropdown", "view-dropdown", "restart-dropdown"]) {
10240
+ const dd = document.getElementById(id);
10241
+ if (dd && !dd.hidden) hideDropdownHard(dd);
10242
+ }
10243
+ }
9928
10244
  function refreshToolbarOverlayShield() {
9929
10245
  const dropdownOpen = ["settings-dropdown", "view-dropdown", "restart-dropdown"].some((id) => {
9930
10246
  const el = document.getElementById(id);
@@ -9940,16 +10256,46 @@ function refreshToolbarOverlayShield() {
9940
10256
  shield.addEventListener("pointerdown", (ev) => {
9941
10257
  ev.preventDefault();
9942
10258
  ev.stopPropagation();
9943
- for (const id of ["settings-dropdown", "view-dropdown", "restart-dropdown"]) {
9944
- const dd = document.getElementById(id);
9945
- if (dd) dd.hidden = true;
9946
- }
9947
- shield?.remove();
10259
+ closeToolbarDropdowns();
10260
+ refreshToolbarOverlayShield();
9948
10261
  });
9949
10262
  document.body.appendChild(shield);
9950
10263
  }
9951
- } else if (shield) {
9952
- shield.remove();
10264
+ if (!tbEscapeCleanup) {
10265
+ const docs = [];
10266
+ document.querySelectorAll(".compose-overlay iframe, .popout-overlay iframe").forEach((ifr) => {
10267
+ try {
10268
+ if (ifr.contentDocument) docs.push(ifr.contentDocument);
10269
+ } catch {
10270
+ }
10271
+ });
10272
+ if (docs.length) {
10273
+ const onEsc = (e) => {
10274
+ if (e.key !== "Escape") return;
10275
+ if (!["settings-dropdown", "view-dropdown", "restart-dropdown"].some((id) => {
10276
+ const el = document.getElementById(id);
10277
+ return el && !el.hidden;
10278
+ })) return;
10279
+ e.preventDefault();
10280
+ e.stopPropagation();
10281
+ closeToolbarDropdowns();
10282
+ refreshToolbarOverlayShield();
10283
+ };
10284
+ for (const d of docs) d.addEventListener("keydown", onEsc, true);
10285
+ tbEscapeCleanup = () => {
10286
+ for (const d of docs) {
10287
+ try {
10288
+ d.removeEventListener("keydown", onEsc, true);
10289
+ } catch {
10290
+ }
10291
+ }
10292
+ tbEscapeCleanup = null;
10293
+ };
10294
+ }
10295
+ }
10296
+ } else {
10297
+ if (shield) shield.remove();
10298
+ if (tbEscapeCleanup) tbEscapeCleanup();
9953
10299
  }
9954
10300
  }
9955
10301
  document.addEventListener("keydown", (e) => {
@@ -9980,18 +10326,21 @@ document.addEventListener("keydown", (e) => {
9980
10326
  var savedTwoLine = localStorage.getItem("mailx-two-line") === "true";
9981
10327
  var savedPreview = localStorage.getItem("mailx-preview") !== "false";
9982
10328
  var savedSnippet = localStorage.getItem("mailx-snippet") !== "false";
10329
+ var savedFlagStars = localStorage.getItem("mailx-flag-stars") !== "false";
9983
10330
  var savedThreaded = localStorage.getItem("mailx-threaded") === "true";
9984
10331
  var savedFlagged = localStorage.getItem("mailx-flagged") === "true";
9985
10332
  var savedFolderCounts = localStorage.getItem("mailx-folder-counts") === "true";
9986
10333
  if (optTwoLine) optTwoLine.checked = savedTwoLine;
9987
10334
  if (optPreview) optPreview.checked = savedPreview;
9988
10335
  if (optSnippet) optSnippet.checked = savedSnippet;
10336
+ if (optFlagStars) optFlagStars.checked = savedFlagStars;
9989
10337
  if (optThreaded) optThreaded.checked = savedThreaded;
9990
10338
  if (optFlagged) optFlagged.checked = savedFlagged;
9991
10339
  if (optFolderCounts) optFolderCounts.checked = savedFolderCounts;
9992
10340
  if (savedTwoLine) document.getElementById("message-list")?.classList.add("two-line");
9993
10341
  if (!savedPreview) document.querySelector(".main-area")?.classList.add("no-preview");
9994
10342
  if (!savedSnippet) document.getElementById("message-list")?.classList.add("no-snippets");
10343
+ if (!savedFlagStars) document.getElementById("message-list")?.classList.add("no-empty-stars");
9995
10344
  if (savedThreaded) document.getElementById("ml-body")?.classList.add("threaded");
9996
10345
  if (savedFlagged) {
9997
10346
  document.getElementById("ml-body")?.classList.add("flagged-only");
@@ -10161,6 +10510,11 @@ optSnippet?.addEventListener("change", () => {
10161
10510
  }
10162
10511
  localStorage.setItem("mailx-snippet", String(optSnippet.checked));
10163
10512
  });
10513
+ optFlagStars?.addEventListener("change", () => {
10514
+ const list = document.getElementById("message-list");
10515
+ list?.classList.toggle("no-empty-stars", !optFlagStars.checked);
10516
+ localStorage.setItem("mailx-flag-stars", String(optFlagStars.checked));
10517
+ });
10164
10518
  document.getElementById("search-help")?.addEventListener("click", async () => {
10165
10519
  const btn = document.getElementById("search-help");
10166
10520
  if (!btn) return;