@bobfrankston/rmfmail 1.1.138 → 1.1.140

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
@@ -937,13 +937,24 @@ async function openCompose(mode: ComposeMode, overrideMsg?: any, overrideAccount
937
937
  }
938
938
 
939
939
 
940
- // Store init data for compose window to pick up. sessionStorage is the
941
- // canonical handoff path same origin between parent and iframe; the
942
- // compose IIFE reads it after the editor finishes booting. We also
943
- // postMessage the iframe so it can short-circuit the listen-for-message
944
- // wait if it's already past editor init.
945
- sessionStorage.setItem("composeInit", JSON.stringify(init));
946
- try { frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*"); } catch { /* */ }
940
+ // Store init data for compose window to pick up. Two parallel paths
941
+ // because sessionStorage propagation to a freshly-loading iframe has
942
+ // intermittently failed under WebView2's custom-protocol host (Bob
943
+ // 2026-05-24: reply-all opened with empty To/Subject; daemon log shows
944
+ // reply-init dump fired correctly but the iframe IIFE saw sessionStorage
945
+ // empty and timed out the wait). postMessage carries the FULL init
946
+ // payload so the iframe can populate even when storage doesn't bridge.
947
+ // sessionStorage remains as the fast-path for iframes that finish
948
+ // loading before postMessage fires.
949
+ const initJson = JSON.stringify(init);
950
+ try { sessionStorage.setItem("composeInit", initJson); }
951
+ catch (e: any) { console.error("[compose] sessionStorage.setItem failed:", e?.message || e); }
952
+ const post = (): void => {
953
+ try { frame?.contentWindow?.postMessage({ type: "compose-init", init }, "*"); } catch { /* */ }
954
+ };
955
+ post();
956
+ // Iframe may not be loaded yet — re-post on load so the listener exists.
957
+ try { frame?.addEventListener("load", post, { once: true }); } catch { /* */ }
947
958
  }
948
959
 
949
960
  function showComposeOverlay(title = "Compose"): HTMLIFrameElement {
@@ -701,7 +701,13 @@ async function createTinyMceEditor(container2, opts = {}) {
701
701
  // everything else so genuine HTML formatting (bold / italic /
702
702
  // tables / inline color) still comes through verbatim.
703
703
  content_style: [
704
- "body { font-family: system-ui, sans-serif; font-size: 14px; }",
704
+ // High-contrast caret: a 1-2 px slate-coloured I-beam vanishes
705
+ // against white especially on hi-DPI displays. caret-color
706
+ // recolours the native insertion bar; caret-shape:block (CSS
707
+ // UI L4, Chromium 125+) widens it into a solid block. The
708
+ // block falls back to the default bar where unsupported, so
709
+ // the color alone still buys most of the visibility win.
710
+ "body { font-family: system-ui, sans-serif; font-size: 14px; caret-color: #d32f2f; caret-shape: block; }",
705
711
  "blockquote { border-left: 3px solid #c0c8d0; margin: 0 0 0 4px; padding: 2px 0 2px 10px; color: #555; }",
706
712
  "div.reply { margin-top: 0.5em; }",
707
713
  "div.reply > p:first-child { color: #666; font-size: 0.95em; margin: 0 0 4px 0; }",
@@ -3723,13 +3729,23 @@ function setupAutocomplete(input) {
3723
3729
  const { start, end } = tokenSpanAtCaret(val, caret);
3724
3730
  return val.substring(start, end).trim();
3725
3731
  }
3726
- function replaceCaretToken(replacement) {
3732
+ function replaceCaretToken(replacement, contact) {
3727
3733
  const val = input.value;
3728
3734
  const caret = input.selectionStart ?? val.length;
3729
3735
  const { start, end } = tokenSpanAtCaret(val, caret);
3730
3736
  let before = val.substring(0, start);
3731
- const after = val.substring(end);
3737
+ let after = val.substring(end);
3732
3738
  before = before.replace(/[ \t,]+$/, "");
3739
+ if (contact) {
3740
+ const haystack = `${contact.name} ${contact.email}`.toLowerCase();
3741
+ const stripStranded = (s) => splitRecipients(s).map((p) => p.trim()).filter((p) => p.length > 0 && (p.includes("@") || !haystack.includes(p.toLowerCase()))).join(", ");
3742
+ if (before) before = stripStranded(before);
3743
+ const afterCore = after.replace(/^[\s,]+/, "").replace(/[\s,]+$/, "");
3744
+ if (afterCore) {
3745
+ const cleanedAfter = stripStranded(afterCore);
3746
+ after = cleanedAfter ? ", " + cleanedAfter : "";
3747
+ }
3748
+ }
3733
3749
  const lead = before.length ? ", " : "";
3734
3750
  const isLast = after.trim() === "";
3735
3751
  const insert = lead + replacement + (isLast ? ", " : "");
@@ -3812,7 +3828,7 @@ function setupAutocomplete(input) {
3812
3828
  e.preventDefault();
3813
3829
  if (e.button !== 0) return;
3814
3830
  const display = formatRecipient(r.name, r.email);
3815
- replaceCaretToken(display);
3831
+ replaceCaretToken(display, { name: r.name, email: r.email });
3816
3832
  });
3817
3833
  item.addEventListener("contextmenu", (e) => {
3818
3834
  e.preventDefault();
@@ -4148,12 +4164,18 @@ function scheduleDraftSave() {
4148
4164
  });
4149
4165
  }, DRAFT_INPUT_DEBOUNCE_MS);
4150
4166
  }
4167
+ var _postedInit = null;
4151
4168
  var _parentInitReady = !!sessionStorage.getItem("composeInit");
4152
4169
  var _parentInitListeners = [];
4153
4170
  window.addEventListener("message", (e) => {
4154
- if (e.data?.type !== "compose-init-ready") return;
4155
- _parentInitReady = true;
4156
- for (const fn of _parentInitListeners.splice(0)) fn();
4171
+ if (e.data?.type === "compose-init-ready") {
4172
+ _parentInitReady = true;
4173
+ for (const fn of _parentInitListeners.splice(0)) fn();
4174
+ } else if (e.data?.type === "compose-init" && e.data.init) {
4175
+ _postedInit = e.data.init;
4176
+ _parentInitReady = true;
4177
+ for (const fn of _parentInitListeners.splice(0)) fn();
4178
+ }
4157
4179
  });
4158
4180
  function waitForParentInit(maxMs) {
4159
4181
  if (_parentInitReady) return Promise.resolve();
@@ -4170,16 +4192,17 @@ function waitForParentInit(maxMs) {
4170
4192
  }
4171
4193
  (async () => {
4172
4194
  _ctick("init IIFE start");
4173
- if (!sessionStorage.getItem("composeInit")) {
4195
+ if (!sessionStorage.getItem("composeInit") && !_postedInit) {
4174
4196
  _ctick("waiting for parent init");
4175
4197
  await waitForParentInit(1500);
4176
4198
  _ctick("parent init received");
4177
4199
  }
4178
4200
  const stored = sessionStorage.getItem("composeInit");
4179
- if (stored) {
4201
+ const initRaw = _postedInit || (stored ? JSON.parse(stored) : null);
4202
+ if (initRaw) {
4180
4203
  sessionStorage.removeItem("composeInit");
4181
- const init = JSON.parse(stored);
4182
- _ctick(`init parsed (mode=${init.mode}, bodyHtml=${init.bodyHtml?.length || 0} bytes)`);
4204
+ const init = initRaw;
4205
+ _ctick(`init parsed (mode=${init.mode}, bodyHtml=${init.bodyHtml?.length || 0} bytes, src=${_postedInit ? "postMessage" : "sessionStorage"})`);
4183
4206
  if (init.accounts && init.accounts.length > 0) {
4184
4207
  applyInit(init);
4185
4208
  _ctick("applyInit done \u2014 compose visible");