@bobfrankston/rmfmail 1.1.199 → 1.1.201

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.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Wires together all UI components and WebSocket connection.
4
4
  */
5
5
  import { initFolderTree, refreshFolderTree, updateFolderCounts, setFolderSynced, getFolderSynced, setOutboxTotal } from "./components/folder-tree.js";
6
- import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView, refreshPriorityIndex } from "./components/message-list.js";
6
+ import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, setLiveFilter, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView, refreshPriorityIndex } from "./components/message-list.js";
7
7
  import { seenOf, flaggedOf, draftOf, setSeen, setFlagged } from "@bobfrankston/mailx-types";
8
8
  import { initTabs, setActiveView as setActiveTabView, openTab } from "./components/tabs.js";
9
9
  import { getCurrentMessage, initViewer, popOutCurrentMessage, printCurrentMessage, toggleFullscreenPreview, showPreviewBodyMenu, wrapHtmlBody } from "./components/message-viewer.js";
@@ -860,7 +860,7 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
860
860
  // anything missing. Don't show "still loading" alerts — the message IS
861
861
  // loaded (it's in the list), body is a separate fetch that isn't needed
862
862
  // for Reply's headers. Missing fields become empty strings.
863
- if ((mode === "reply" || mode === "replyAll" || mode === "forward") && !current) {
863
+ if ((mode === "reply" || mode === "replyAll" || mode === "forward" || mode === "editAsNew") && !current) {
864
864
  // Only true blocker: no message selected at all.
865
865
  console.warn(`[compose] ${mode} — no message selected`);
866
866
  return;
@@ -881,7 +881,8 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
881
881
  const titlePrefix = mode === "reply" ? "Reply" :
882
882
  mode === "replyAll" ? "Reply All" :
883
883
  mode === "forward" ? "Forward" :
884
- "Compose";
884
+ mode === "editAsNew" ? "Edit as new" :
885
+ "Compose";
885
886
  const titleSubject = mode === "new" ? "" : (msg?.subject || "");
886
887
  const frame = showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
887
888
  // Now finish initialisation off the critical path — editor bootstrap
@@ -1032,6 +1033,18 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
1032
1033
  init.bodyHtml = forwardBody(msg);
1033
1034
  init.fromAddress = detectReplyFrom();
1034
1035
  }
1036
+ else if (msg && mode === "editAsNew") {
1037
+ // Edit-as-new clones an existing message into a fresh compose as if
1038
+ // the user authored it: original To/Cc/Subject/body, all editable.
1039
+ // No `Re:`/`Fwd:` prefix (subject verbatim), no quote/forward wrapper,
1040
+ // no In-Reply-To / References (this is NOT a reply — it gets a brand-
1041
+ // new Message-ID at send time). Replaces the move-to-Drafts kludge for
1042
+ // "tweak this and send it again" (Q152, Bob 2026-05-31).
1043
+ init.to = Array.isArray(msg.to) ? msg.to : [];
1044
+ init.cc = Array.isArray(msg.cc) ? msg.cc : [];
1045
+ init.subject = msg.subject || "";
1046
+ init.bodyHtml = editAsNewBody(msg);
1047
+ }
1035
1048
  // Store init data for compose window to pick up. Two parallel paths
1036
1049
  // because sessionStorage propagation to a freshly-loading iframe has
1037
1050
  // intermittently failed under WebView2's custom-protocol host (Bob
@@ -1393,6 +1406,13 @@ function quoteBody(msg) {
1393
1406
  // inside it.
1394
1407
  return `<p></p><br><br><div class="reply"><p>On ${date}, ${from} wrote:</p><blockquote>${body}</blockquote></div>`;
1395
1408
  }
1409
+ function editAsNewBody(msg) {
1410
+ // The message body verbatim (same plain-text/HTML cleaning a quoted reply
1411
+ // gets), with NO attribution line and NO quote/forward wrapper — the user
1412
+ // is re-authoring this content, not quoting it. A leading empty paragraph
1413
+ // gives the caret a block to land in above the cloned content.
1414
+ return `<p></p>${sanitizeQuotedBody(msg)}`;
1415
+ }
1396
1416
  function forwardBody(msg) {
1397
1417
  const date = new Date(msg.date).toLocaleString();
1398
1418
  const from = msg.from.name ? `${msg.from.name} &lt;${msg.from.address}&gt;` : msg.from.address;
@@ -2220,14 +2240,12 @@ function doSearch(immediate = false) {
2220
2240
  // Only when the server checkbox is OFF — with it on we want the real
2221
2241
  // local+server search, not a row filter.
2222
2242
  if (localScope === "current" && !serverOn && !immediate) {
2223
- const body = document.getElementById("ml-body");
2224
- if (body) {
2225
- const lower = query.toLowerCase();
2226
- for (const row of body.querySelectorAll(".ml-row")) {
2227
- const text = row.textContent?.toLowerCase() || "";
2228
- row.classList.toggle("filter-hidden", !text.includes(lower));
2229
- }
2230
- }
2243
+ // Register as a live list filter (not a raw DOM toggle) so it survives
2244
+ // background sync reloads and blocks reloadCurrentFolder — otherwise a
2245
+ // sync mid-read wipes the filter, deselects the open message, and pops
2246
+ // back to the full list while the search box still shows the query
2247
+ // (Bob 2026-05-31). setLiveFilter applies the hide to current rows.
2248
+ setLiveFilter(query);
2231
2249
  return;
2232
2250
  }
2233
2251
  // ── Phase 1: LOCAL search — always, immediately ──