@bobfrankston/rmfmail 1.2.247 → 1.2.250

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 (53) hide show
  1. package/client/app.bundle.js +239 -50
  2. package/client/app.bundle.js.map +4 -4
  3. package/client/app.js +23 -36
  4. package/client/app.js.map +1 -1
  5. package/client/app.ts +17 -36
  6. package/client/components/folder-picker.js +3 -2
  7. package/client/components/folder-picker.js.map +1 -1
  8. package/client/components/folder-picker.ts +3 -2
  9. package/client/components/folder-tree.js +90 -12
  10. package/client/components/folder-tree.js.map +1 -1
  11. package/client/components/folder-tree.ts +89 -12
  12. package/client/components/message-list.js +35 -2
  13. package/client/components/message-list.js.map +1 -1
  14. package/client/components/message-list.ts +32 -2
  15. package/client/components/message-viewer.js +149 -11
  16. package/client/components/message-viewer.js.map +1 -1
  17. package/client/components/message-viewer.ts +139 -11
  18. package/client/compose/compose.bundle.js +11 -0
  19. package/client/compose/compose.bundle.js.map +2 -2
  20. package/client/help/search-help.js +6 -1
  21. package/client/help/search-help.js.map +1 -1
  22. package/client/help/search-help.ts +6 -1
  23. package/client/lib/api-client.js +14 -0
  24. package/client/lib/api-client.js.map +1 -1
  25. package/client/lib/api-client.ts +9 -0
  26. package/client/lib/fold-text.js +18 -0
  27. package/client/lib/fold-text.js.map +1 -0
  28. package/client/lib/fold-text.ts +17 -0
  29. package/client/styles/components.css +23 -3
  30. package/package.json +1 -1
  31. package/packages/mailx-imap/package-lock.json +2 -2
  32. package/packages/mailx-imap/package.json +1 -1
  33. package/packages/mailx-service/index.d.ts +16 -0
  34. package/packages/mailx-service/index.d.ts.map +1 -1
  35. package/packages/mailx-service/index.js +49 -1
  36. package/packages/mailx-service/index.js.map +1 -1
  37. package/packages/mailx-service/index.ts +48 -1
  38. package/packages/mailx-service/jsonrpc.js +2 -0
  39. package/packages/mailx-service/jsonrpc.js.map +1 -1
  40. package/packages/mailx-service/jsonrpc.ts +2 -0
  41. package/packages/mailx-service/package.json +1 -1
  42. package/packages/mailx-settings/package.json +1 -1
  43. package/packages/mailx-store/db.d.ts.map +1 -1
  44. package/packages/mailx-store/db.js +5 -2
  45. package/packages/mailx-store/db.js.map +1 -1
  46. package/packages/mailx-store/db.ts +5 -2
  47. package/packages/mailx-store/package.json +1 -1
  48. package/packages/mailx-store-web/package.json +1 -1
  49. package/packages/mailx-types/mailx-api.d.ts +7 -0
  50. package/packages/mailx-types/mailx-api.d.ts.map +1 -1
  51. package/packages/mailx-types/mailx-api.ts +4 -0
  52. package/packages/mailx-types/package.json +1 -1
  53. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-16772 → node_modules.npmglobalize-stash-52956}/.package-lock.json +0 -0
@@ -52,6 +52,7 @@ __export(api_client_exports, {
52
52
  drainStoreSync: () => drainStoreSync,
53
53
  emptyFolder: () => emptyFolder,
54
54
  fetchImageAsDataUri: () => fetchImageAsDataUri,
55
+ fetchRemoteImage: () => fetchRemoteImage,
55
56
  flagSenderOrDomain: () => flagSenderOrDomain,
56
57
  formatJsonc: () => formatJsonc,
57
58
  getAccounts: () => getAccounts,
@@ -614,6 +615,16 @@ function readConfigHelp(name) {
614
615
  function unsubscribeOneClick(url) {
615
616
  return ipc().unsubscribeOneClick?.(url);
616
617
  }
618
+ async function fetchRemoteImage(url) {
619
+ const fn = ipc().fetchRemoteImage;
620
+ if (!fn)
621
+ return { error: "this host can't fetch remote images" };
622
+ try {
623
+ return await fn(url);
624
+ } catch (e) {
625
+ return { error: e?.message || String(e) };
626
+ }
627
+ }
617
628
  function openInWord(editId, html, popoutId = "") {
618
629
  return ipc().openInWord?.(editId, html, popoutId) ?? Promise.resolve({ ok: false, path: "", opener: "none" });
619
630
  }
@@ -1163,6 +1174,16 @@ var init_context_menu = __esm({
1163
1174
  }
1164
1175
  });
1165
1176
 
1177
+ // client/lib/fold-text.js
1178
+ function foldText(s) {
1179
+ return (s || "").normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
1180
+ }
1181
+ var init_fold_text = __esm({
1182
+ "client/lib/fold-text.js"() {
1183
+ "use strict";
1184
+ }
1185
+ });
1186
+
1166
1187
  // client/lib/message-state.js
1167
1188
  var message_state_exports = {};
1168
1189
  __export(message_state_exports, {
@@ -1639,6 +1660,28 @@ __export(message_viewer_exports, {
1639
1660
  toggleFullscreenPreview: () => toggleFullscreenPreview,
1640
1661
  wrapHtmlBody: () => wrapHtmlBody
1641
1662
  });
1663
+ function requestImageFromPreview(sourceWindow, src) {
1664
+ if (!sourceWindow)
1665
+ return Promise.resolve({ error: "preview window is gone" });
1666
+ const reqId = ++imageCopySeq;
1667
+ return new Promise((resolve) => {
1668
+ const done = (r) => {
1669
+ clearTimeout(timer);
1670
+ resolve(r);
1671
+ };
1672
+ const timer = setTimeout(() => {
1673
+ pendingImageCopies.delete(reqId);
1674
+ resolve({ error: "the preview did not answer in time" });
1675
+ }, IMAGE_COPY_TIMEOUT_MS);
1676
+ pendingImageCopies.set(reqId, done);
1677
+ try {
1678
+ sourceWindow.postMessage({ type: "previewCommand", cmd: "copyImage", reqId, src }, "*");
1679
+ } catch (err) {
1680
+ pendingImageCopies.delete(reqId);
1681
+ done({ error: err?.message || String(err) });
1682
+ }
1683
+ });
1684
+ }
1642
1685
  function parsedCacheGet(accountId, folderId, uid) {
1643
1686
  if (folderId == null)
1644
1687
  return null;
@@ -1760,19 +1803,56 @@ function showPreviewBodyMenu(absX, absY, selectedText, sourceWindow, linkUrl, li
1760
1803
  }
1761
1804
  })();
1762
1805
  items.push({ label: "Copy image", action: async () => {
1763
- try {
1764
- const resp = await fetch(imgSrc);
1765
- const blob = await resp.blob();
1806
+ const attempts = [];
1807
+ const writePng = async (png) => {
1808
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
1809
+ };
1810
+ const toPng = async (blob) => {
1811
+ if (blob.type === "image/png")
1812
+ return blob;
1766
1813
  const bmp = await createImageBitmap(blob);
1767
1814
  const canvas = document.createElement("canvas");
1768
1815
  canvas.width = bmp.width;
1769
1816
  canvas.height = bmp.height;
1770
1817
  canvas.getContext("2d").drawImage(bmp, 0, 0);
1771
- const png = await new Promise((res, rej) => canvas.toBlob((b) => b ? res(b) : rej(new Error("toBlob failed")), "image/png"));
1772
- await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
1818
+ return await new Promise((res, rej) => canvas.toBlob((b) => b ? res(b) : rej(new Error("toBlob failed")), "image/png"));
1819
+ };
1820
+ const fromPreview = await requestImageFromPreview(sourceWindow, imgSrc);
1821
+ if (fromPreview.blob) {
1822
+ try {
1823
+ await writePng(fromPreview.blob);
1824
+ return;
1825
+ } catch (e) {
1826
+ attempts.push(`clipboard write: ${e?.message || e}`);
1827
+ }
1828
+ } else {
1829
+ attempts.push(`preview: ${fromPreview.error}`);
1830
+ }
1831
+ try {
1832
+ const resp = await fetch(imgSrc);
1833
+ if (!resp.ok)
1834
+ throw new Error(`HTTP ${resp.status}`);
1835
+ await writePng(await toPng(await resp.blob()));
1836
+ return;
1773
1837
  } catch (e) {
1774
- alert(`Couldn't copy image: ${e?.message || e}${imgSrc.startsWith("cid:") ? " (inline cid: image \u2014 try Save instead, or right-click \u2192 save in the message)" : ""}`);
1838
+ attempts.push(`direct fetch: ${e?.message || e}`);
1775
1839
  }
1840
+ if (/^https?:/i.test(imgSrc)) {
1841
+ try {
1842
+ const { fetchRemoteImage: fetchRemoteImage2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
1843
+ const r = await fetchRemoteImage2(imgSrc);
1844
+ if (!r?.base64)
1845
+ throw new Error(r?.error || "no data returned");
1846
+ const bytes = Uint8Array.from(atob(r.base64), (c) => c.charCodeAt(0));
1847
+ await writePng(await toPng(new Blob([bytes], { type: r.contentType || "image/png" })));
1848
+ return;
1849
+ } catch (e) {
1850
+ attempts.push(`via rmfmail: ${e?.message || e}`);
1851
+ }
1852
+ }
1853
+ alert(`Couldn't copy that image.
1854
+
1855
+ ${attempts.map((a) => `\u2022 ${a}`).join("\n")}`);
1776
1856
  } }, { label: `Save image as "${guessImgName}"\u2026`, action: () => {
1777
1857
  const a = document.createElement("a");
1778
1858
  a.href = imgSrc;
@@ -3305,6 +3385,9 @@ ${csp}
3305
3385
  // dismissers it sees. The parent's debounced 500 ms show-timer still
3306
3386
  // suppresses flicker.
3307
3387
  var lastHoveredHref = "";
3388
+ // The <img> under the most recent right-click, kept so a later
3389
+ // "copyImage" command can encode that exact element (see below).
3390
+ var lastContextImg = null;
3308
3391
  function postLinkHover(href, rect, mouse) {
3309
3392
  // Only post on transitions to avoid spamming the parent on every
3310
3393
  // mousemove inside a link.
@@ -3380,6 +3463,45 @@ ${csp}
3380
3463
  s.addRange(r);
3381
3464
  }
3382
3465
  } catch (_) {}
3466
+ } else if (d.cmd === "copyImage") {
3467
+ // Encode the right-clicked image HERE and hand the parent the
3468
+ // bytes. The parent can't fetch it itself: an inline image is a
3469
+ // multi-megabyte data: URI it would have to re-download through
3470
+ // the message channel, and a remote one is cross-origin with no
3471
+ // CORS headers, so a parent-side fetch rejects. This img is
3472
+ // already decoded here, so a canvas draw needs no network at all.
3473
+ // (A remote image DOES taint the canvas \u2014 toBlob then throws and
3474
+ // the parent falls back to fetching it through the daemon.)
3475
+ var target = lastContextImg;
3476
+ if (!target && d.src) {
3477
+ var all = document.getElementsByTagName("img");
3478
+ for (var i = 0; i < all.length; i++) {
3479
+ if (all[i].currentSrc === d.src || all[i].src === d.src) { target = all[i]; break; }
3480
+ }
3481
+ }
3482
+ if (!target) {
3483
+ window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, error: "image element not found" }, "*");
3484
+ return;
3485
+ }
3486
+ try {
3487
+ var cv = document.createElement("canvas");
3488
+ cv.width = target.naturalWidth || target.width;
3489
+ cv.height = target.naturalHeight || target.height;
3490
+ if (!cv.width || !cv.height) throw new Error("image has no decoded size yet");
3491
+ cv.getContext("2d").drawImage(target, 0, 0);
3492
+ cv.toBlob(function (b) {
3493
+ if (!b) {
3494
+ window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, error: "encode failed" }, "*");
3495
+ return;
3496
+ }
3497
+ window.parent.postMessage({ type: "previewImageBytes", reqId: d.reqId, blob: b }, "*");
3498
+ }, "image/png");
3499
+ } catch (err) {
3500
+ window.parent.postMessage({
3501
+ type: "previewImageBytes", reqId: d.reqId,
3502
+ error: (err && err.message) ? err.message : String(err)
3503
+ }, "*");
3504
+ }
3383
3505
  }
3384
3506
  });
3385
3507
 
@@ -3398,6 +3520,10 @@ ${csp}
3398
3520
  try { rect = e.target.getBoundingClientRect ? e.target.getBoundingClientRect() : rect; } catch (_) {}
3399
3521
  var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
3400
3522
  var img = e.target && e.target.closest ? e.target.closest("img") : null;
3523
+ // Remember the element itself, not just its src \u2014 "Copy image" encodes
3524
+ // this exact <img>, and matching by src would pick the wrong one when
3525
+ // a letter repeats the same image (spacers, logos, tracking pixels).
3526
+ lastContextImg = img;
3401
3527
  var sel = (window.getSelection && window.getSelection()) ? window.getSelection().toString() : "";
3402
3528
  window.parent.postMessage({
3403
3529
  type: "previewContextMenu",
@@ -3723,7 +3849,7 @@ function spawnDesktopPopout(msg, accountId) {
3723
3849
  function escapeHtmlLocal(s) {
3724
3850
  return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3725
3851
  }
3726
- var currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, WAIT_GIVE_UP_SECS, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom, RENDER_MARK_KEY, RENDER_MARK_MAX_AGE_MS, poisonedMessages, renderKey, PROGRESSIVE_TEXT_THRESHOLD, TEXT_CHUNK_BYTES;
3852
+ var pendingImageCopies, imageCopySeq, IMAGE_COPY_TIMEOUT_MS, currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, WAIT_GIVE_UP_SECS, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom, RENDER_MARK_KEY, RENDER_MARK_MAX_AGE_MS, poisonedMessages, renderKey, PROGRESSIVE_TEXT_THRESHOLD, TEXT_CHUNK_BYTES;
3727
3853
  var init_message_viewer = __esm({
3728
3854
  "client/components/message-viewer.js"() {
3729
3855
  "use strict";
@@ -3732,6 +3858,19 @@ var init_message_viewer = __esm({
3732
3858
  init_message_state();
3733
3859
  init_message_list();
3734
3860
  init_mailx_types();
3861
+ pendingImageCopies = /* @__PURE__ */ new Map();
3862
+ imageCopySeq = 0;
3863
+ IMAGE_COPY_TIMEOUT_MS = 8e3;
3864
+ window.addEventListener("message", (e) => {
3865
+ const d = e.data;
3866
+ if (!d || d.type !== "previewImageBytes")
3867
+ return;
3868
+ const resolve = pendingImageCopies.get(d.reqId);
3869
+ if (!resolve)
3870
+ return;
3871
+ pendingImageCopies.delete(d.reqId);
3872
+ resolve({ blob: d.blob, error: d.error });
3873
+ });
3735
3874
  currentMessage = null;
3736
3875
  currentAccountId = "";
3737
3876
  dragOutUrls = [];
@@ -3909,7 +4048,7 @@ function pickFolder(accountId, opts) {
3909
4048
  const targets = folders.filter((f) => !excluded.has(f.id)).filter((f) => f.specialUse !== "outbox").sort((a, b) => a.path.localeCompare(b.path));
3910
4049
  function render2(filter) {
3911
4050
  listEl.innerHTML = "";
3912
- const lc = filter.toLowerCase().trim();
4051
+ const lc = foldText(filter.trim());
3913
4052
  let firstMatchSet = false;
3914
4053
  for (const f of targets) {
3915
4054
  const row = document.createElement("div");
@@ -3922,7 +4061,7 @@ function pickFolder(accountId, opts) {
3922
4061
  tag.textContent = f.specialUse || "";
3923
4062
  row.appendChild(name);
3924
4063
  row.appendChild(tag);
3925
- const matches = !lc || f.path.toLowerCase().includes(lc);
4064
+ const matches = !lc || foldText(f.path).includes(lc) || foldText(f.name).includes(lc);
3926
4065
  if (matches) {
3927
4066
  row.classList.add("match");
3928
4067
  if (!firstMatchSet) {
@@ -3949,6 +4088,7 @@ var init_folder_picker = __esm({
3949
4088
  "client/components/folder-picker.js"() {
3950
4089
  "use strict";
3951
4090
  init_api_client();
4091
+ init_fold_text();
3952
4092
  }
3953
4093
  });
3954
4094
 
@@ -3980,7 +4120,7 @@ __export(message_list_exports, {
3980
4120
  showThreadPopup: () => showThreadPopup
3981
4121
  });
3982
4122
  function setLiveFilter(query) {
3983
- liveFilterText = (query || "").trim().toLowerCase();
4123
+ liveFilterText = foldText((query || "").trim());
3984
4124
  const body = document.getElementById("ml-body");
3985
4125
  if (body)
3986
4126
  applyLiveFilter(body);
@@ -3993,7 +4133,7 @@ function applyLiveFilter(body) {
3993
4133
  return;
3994
4134
  }
3995
4135
  for (const row of body.querySelectorAll(".ml-row")) {
3996
- const text = row.textContent?.toLowerCase() || "";
4136
+ const text = foldText(row.textContent);
3997
4137
  row.classList.toggle("filter-hidden", !text.includes(liveFilterText));
3998
4138
  }
3999
4139
  }
@@ -5094,6 +5234,17 @@ function appendMessages(body, accountId, items) {
5094
5234
  row.attach(body);
5095
5235
  }
5096
5236
  }
5237
+ function formatSize2(bytes) {
5238
+ if (!bytes || bytes <= 0)
5239
+ return "";
5240
+ if (bytes < 1024)
5241
+ return "<1K";
5242
+ const kb = bytes / 1024;
5243
+ if (kb < 1024)
5244
+ return `${Math.round(kb)}K`;
5245
+ const mb = kb / 1024;
5246
+ return mb < 10 ? `${mb.toFixed(1)}M` : `${Math.round(mb)}M`;
5247
+ }
5097
5248
  function formatDate(epochMs) {
5098
5249
  const d = new Date(epochMs);
5099
5250
  const now = /* @__PURE__ */ new Date();
@@ -5119,6 +5270,7 @@ var init_message_list = __esm({
5119
5270
  init_message_viewer();
5120
5271
  init_context_menu();
5121
5272
  init_folder_picker();
5273
+ init_fold_text();
5122
5274
  init_mailx_types();
5123
5275
  currentSpecialUse = "";
5124
5276
  lastClickedRow = null;
@@ -5282,11 +5434,17 @@ var init_message_list = __esm({
5282
5434
  const icons = document.createElement("span");
5283
5435
  icons.className = "ml-status-icons";
5284
5436
  renderStatusIcons(icons, msg);
5437
+ const size = document.createElement("span");
5438
+ size.className = "ml-size";
5439
+ size.textContent = formatSize2(msg.size);
5440
+ if (msg.size > 0)
5441
+ size.title = `${msg.size.toLocaleString()} bytes`;
5285
5442
  const dateText = document.createElement("span");
5286
5443
  dateText.className = "ml-date-text";
5287
5444
  dateText.textContent = formatDate(effectiveDate(msg));
5288
5445
  dateText.title = dateBasis === "received" ? "Received date" : "Sent date";
5289
5446
  date.appendChild(icons);
5447
+ date.appendChild(size);
5290
5448
  date.appendChild(dateText);
5291
5449
  row.appendChild(avatar);
5292
5450
  row.appendChild(flag);
@@ -7270,7 +7428,7 @@ decides which operators work.</p>
7270
7428
  <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>
7271
7429
  <tr><td><code>has:attachment</code></td><td>Only messages that carry a file attachment</td></tr>
7272
7430
  <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>
7273
- <tr><td><code>folder:sent</code></td><td>Restrict to folders whose name contains the term</td></tr>
7431
+ <tr><td><code>folder:sent</code></td><td>Restrict to folders whose <em>name or path</em> contains the term \u2014 so <code>folder:rmf</code> and <code>folder:lists/rmf</code> both work</td></tr>
7274
7432
  <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>
7275
7433
  </table>
7276
7434
  <p>Any remaining unqualified text is the free-text term.</p>
@@ -7310,6 +7468,11 @@ results only.</p>
7310
7468
  <p>All three modes \u2014 local FTS5, IMAP server SEARCH, and <code>/regex/</code> \u2014
7311
7469
  are <strong>case-insensitive</strong>. <code>Lunch</code>, <code>lunch</code>, and
7312
7470
  <code>LUNCH</code> match the same messages. There is no case-sensitive mode.</p>
7471
+ <p>The sidebar <strong>Find folder</strong> box and the <strong>Move to folder</strong>
7472
+ picker go further: they also ignore accents, so <code>archiv</code> finds
7473
+ <code>\xC4rchiv</code>. Find folder searches every account's whole folder list \u2014
7474
+ collapsed folders and collapsed accounts included \u2014 and opens the tree to
7475
+ whatever it finds.</p>
7313
7476
 
7314
7477
  <h3>Limitations</h3>
7315
7478
  <ul>
@@ -7458,6 +7621,7 @@ function setActiveView(view, title) {
7458
7621
  }
7459
7622
 
7460
7623
  // client/components/folder-tree.js
7624
+ init_fold_text();
7461
7625
  var onFolderSelect;
7462
7626
  var onUnifiedInbox = null;
7463
7627
  var selectedElement;
@@ -7477,6 +7641,29 @@ function collapseDragExpanded() {
7477
7641
  dragAutoExpanded.clear();
7478
7642
  return true;
7479
7643
  }
7644
+ var folderFilter = "";
7645
+ function setFolderFilter(query) {
7646
+ const next = foldText(query.trim());
7647
+ if (next === folderFilter)
7648
+ return;
7649
+ folderFilter = next;
7650
+ const container = document.getElementById("folder-tree");
7651
+ if (container)
7652
+ loadFolderTree(container);
7653
+ }
7654
+ function pruneToMatches(nodes, q, forceExpand) {
7655
+ const kept = [];
7656
+ for (const n of nodes) {
7657
+ const selfMatch = foldText(n.name).includes(q) || foldText(n.path).includes(q);
7658
+ const keptChildren = pruneToMatches(n.children, q, forceExpand);
7659
+ if (!selfMatch && keptChildren.length === 0)
7660
+ continue;
7661
+ kept.push({ ...n, children: selfMatch ? n.children : keptChildren });
7662
+ if (keptChildren.length > 0)
7663
+ forceExpand.add(`${n.accountId}:${n.path}`);
7664
+ }
7665
+ return kept;
7666
+ }
7480
7667
  var expandState = JSON.parse(localStorage.getItem("mailx-folders-expanded") || "{}");
7481
7668
  function saveExpandState() {
7482
7669
  localStorage.setItem("mailx-folders-expanded", JSON.stringify(expandState));
@@ -7613,10 +7800,10 @@ function sortFolders(nodes) {
7613
7800
  sortFolders(n.children);
7614
7801
  }
7615
7802
  }
7616
- function renderNode(node, container, depth) {
7803
+ function renderNode(node, container, depth, forceExpand) {
7617
7804
  const hasChildren = node.children.length > 0;
7618
7805
  const expandKey = `${node.accountId}:${node.path}`;
7619
- const isExpanded = expandState[expandKey] === true;
7806
+ const isExpanded = forceExpand?.has(expandKey) ? expandState[expandKey] !== false : expandState[expandKey] === true;
7620
7807
  const folderEl = document.createElement("div");
7621
7808
  folderEl.className = "ft-folder";
7622
7809
  folderEl.dataset.accountId = node.accountId;
@@ -7970,7 +8157,7 @@ function renderNode(node, container, depth) {
7970
8157
  container.appendChild(folderEl);
7971
8158
  if (hasChildren && isExpanded) {
7972
8159
  for (const child of node.children) {
7973
- renderNode(child, container, depth + 1);
8160
+ renderNode(child, container, depth + 1, forceExpand);
7974
8161
  }
7975
8162
  }
7976
8163
  }
@@ -8300,12 +8487,12 @@ async function loadFolderTree(container) {
8300
8487
  const accountFolderData = await Promise.all(accounts.map(async (account) => {
8301
8488
  const accountKey = `account:${account.id}`;
8302
8489
  const accountExpanded = expandState[accountKey] !== false;
8303
- const folders = accountExpanded ? await getFolders(account.id) : [];
8490
+ const folders = accountExpanded || folderFilter ? await getFolders(account.id) : [];
8304
8491
  return { account, folders };
8305
8492
  }));
8306
8493
  const savedScroll = container.scrollTop;
8307
8494
  const fragment = document.createDocumentFragment();
8308
- if (accounts.length >= 1) {
8495
+ if (accounts.length >= 1 && (!folderFilter || foldText("All Inboxes").includes(folderFilter))) {
8309
8496
  const unifiedEl = document.createElement("div");
8310
8497
  unifiedEl.className = "ft-folder ft-unified";
8311
8498
  unifiedEl.title = accounts.length > 1 ? "Merged inbox view of all accounts \u2014 click to see messages from every account's INBOX sorted by date" : "Inbox view across all your accounts";
@@ -8322,7 +8509,7 @@ async function loadFolderTree(container) {
8322
8509
  });
8323
8510
  fragment.appendChild(unifiedEl);
8324
8511
  }
8325
- if (lastOutboxTotal > 0) {
8512
+ if (lastOutboxTotal > 0 && (!folderFilter || foldText("Send-pending").includes(folderFilter))) {
8326
8513
  const pendingEl = document.createElement("div");
8327
8514
  pendingEl.className = "ft-folder ft-unified ft-send-pending";
8328
8515
  pendingEl.id = "ft-send-pending";
@@ -8347,7 +8534,7 @@ async function loadFolderTree(container) {
8347
8534
  const accountEl = document.createElement("div");
8348
8535
  accountEl.className = "ft-account";
8349
8536
  const accountKey = `account:${account.id}`;
8350
- const accountExpanded = expandState[accountKey] !== false;
8537
+ const accountExpanded = folderFilter ? true : expandState[accountKey] !== false;
8351
8538
  const header = document.createElement("div");
8352
8539
  header.className = "ft-account-header";
8353
8540
  const baseLabel = labelOf(account);
@@ -8355,7 +8542,7 @@ async function loadFolderTree(container) {
8355
8542
  const disambiguator = isDup ? ` (${account.email || account.id})` : "";
8356
8543
  header.textContent = `${accountExpanded ? "\u25BE" : "\u25B8"} ${baseLabel}${disambiguator}`;
8357
8544
  header.addEventListener("click", () => {
8358
- expandState[accountKey] = !accountExpanded;
8545
+ expandState[accountKey] = expandState[accountKey] === false;
8359
8546
  saveExpandState();
8360
8547
  const treeContainer = document.getElementById("folder-tree");
8361
8548
  if (treeContainer)
@@ -8412,10 +8599,17 @@ async function loadFolderTree(container) {
8412
8599
  showContextMenu(e.clientX, e.clientY, items, { editTarget: e.target });
8413
8600
  });
8414
8601
  accountEl.appendChild(header);
8602
+ let filteredOut = false;
8415
8603
  if (accountExpanded && folders.length > 0) {
8416
8604
  const delimiter = folders[0]?.delimiter || ".";
8417
- const tree = buildTree(folders, delimiter, account.id);
8605
+ let tree = buildTree(folders, delimiter, account.id);
8418
8606
  sortFolders(tree);
8607
+ const forceExpand = /* @__PURE__ */ new Set();
8608
+ if (folderFilter && !foldText(`${baseLabel} ${account.email || ""} ${account.id}`).includes(folderFilter)) {
8609
+ tree = pruneToMatches(tree, folderFilter, forceExpand);
8610
+ if (tree.length === 0)
8611
+ filteredOut = true;
8612
+ }
8419
8613
  const lowerCounts = /* @__PURE__ */ new Map();
8420
8614
  for (const f of folders) {
8421
8615
  const key = (f.path || "").toLowerCase();
@@ -8426,7 +8620,7 @@ async function loadFolderTree(container) {
8426
8620
  if (c > 1)
8427
8621
  duplicatePaths.add(k);
8428
8622
  for (const node of tree) {
8429
- renderNode(node, accountEl, 1);
8623
+ renderNode(node, accountEl, 1, forceExpand);
8430
8624
  }
8431
8625
  if (duplicatePaths.size > 0) {
8432
8626
  accountEl.querySelectorAll(".ft-folder").forEach((el) => {
@@ -8437,8 +8631,17 @@ async function loadFolderTree(container) {
8437
8631
  }
8438
8632
  });
8439
8633
  }
8634
+ } else if (folderFilter) {
8635
+ filteredOut = true;
8440
8636
  }
8441
- fragment.appendChild(accountEl);
8637
+ if (!filteredOut)
8638
+ fragment.appendChild(accountEl);
8639
+ }
8640
+ if (folderFilter && !fragment.querySelector(".ft-folder")) {
8641
+ const empty = document.createElement("div");
8642
+ empty.className = "ft-filter-empty";
8643
+ empty.textContent = `No folder matches "${document.getElementById("ft-filter-input")?.value || folderFilter}"`;
8644
+ fragment.appendChild(empty);
8442
8645
  }
8443
8646
  container.replaceChildren(fragment);
8444
8647
  container.scrollTop = savedScroll;
@@ -10600,36 +10803,22 @@ document.getElementById("search-server-too")?.addEventListener("change", rerunAc
10600
10803
  document.getElementById("search-include-trash")?.addEventListener("change", rerunActiveSearch);
10601
10804
  var ftFilterInput = document.getElementById("ft-filter-input");
10602
10805
  if (ftFilterInput) {
10603
- ftFilterInput.addEventListener("input", () => {
10604
- const query = ftFilterInput.value.toLowerCase();
10605
- const tree = document.getElementById("folder-tree");
10606
- if (!tree) return;
10607
- if (!query) {
10608
- tree.querySelectorAll(".ft-filter-hidden").forEach((el) => el.classList.remove("ft-filter-hidden"));
10609
- return;
10610
- }
10611
- const folders = tree.querySelectorAll(".ft-folder");
10612
- const accounts = tree.querySelectorAll(".ft-account");
10613
- for (const acct of accounts) acct.classList.add("ft-filter-hidden");
10614
- for (const f of folders) f.classList.add("ft-filter-hidden");
10615
- for (const f of folders) {
10616
- const name = f.querySelector(".ft-folder-name")?.textContent?.toLowerCase() || "";
10617
- if (name.includes(query)) {
10618
- f.classList.remove("ft-filter-hidden");
10619
- const acct = f.closest(".ft-account");
10620
- if (acct) acct.classList.remove("ft-filter-hidden");
10621
- }
10622
- }
10623
- const unified = tree.querySelector(".ft-unified");
10624
- if (unified) {
10625
- const text = unified.textContent?.toLowerCase() || "";
10626
- unified.classList.toggle("ft-filter-hidden", !text.includes(query));
10627
- }
10628
- });
10806
+ let ftFilterTimer = null;
10807
+ const FT_FILTER_DEBOUNCE_MS = 120;
10808
+ const applyFolderFilter = (immediate) => {
10809
+ if (ftFilterTimer) {
10810
+ clearTimeout(ftFilterTimer);
10811
+ ftFilterTimer = null;
10812
+ }
10813
+ const run = () => setFolderFilter(ftFilterInput.value);
10814
+ if (immediate) run();
10815
+ else ftFilterTimer = setTimeout(run, FT_FILTER_DEBOUNCE_MS);
10816
+ };
10817
+ ftFilterInput.addEventListener("input", () => applyFolderFilter(false));
10629
10818
  ftFilterInput.addEventListener("keydown", (e) => {
10630
10819
  if (e.key === "Escape") {
10631
10820
  ftFilterInput.value = "";
10632
- ftFilterInput.dispatchEvent(new Event("input"));
10821
+ applyFolderFilter(true);
10633
10822
  }
10634
10823
  });
10635
10824
  }