@bobfrankston/rmfmail 1.0.680 → 1.0.681

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
@@ -171,9 +171,15 @@ function updateBadge(count: number): void {
171
171
  async function updateNewMessageCount(): Promise<void> {
172
172
  try {
173
173
  const accounts = await getAccounts();
174
+ // Fan out folder queries in parallel — earlier code awaited each
175
+ // account's `getFolders` in series, so an N-account setup paid N
176
+ // back-to-back IPC round-trips on every count refresh (folderCountsChanged,
177
+ // sync events, IDLE updates).
178
+ const folderLists = await Promise.all(
179
+ accounts.map((acct: any) => getFolders(acct.id).catch(() => [] as any[])),
180
+ );
174
181
  let totalUnread = 0;
175
- for (const acct of accounts) {
176
- const folders = await getFolders(acct.id);
182
+ for (const folders of folderLists) {
177
183
  const inbox = folders.find((f: any) => f.specialUse === "inbox");
178
184
  if (inbox) totalUnread += inbox.unreadCount || 0;
179
185
  }
@@ -242,9 +248,12 @@ window.addEventListener("focus", stopTitleFlash);
242
248
  /** Call when user actively views messages — resets the badge */
243
249
  function markAsSeen(): void {
244
250
  getAccounts().then(async (accounts: any[]) => {
251
+ // Parallel folder fetch — see updateNewMessageCount for rationale.
252
+ const folderLists = await Promise.all(
253
+ accounts.map((acct: any) => getFolders(acct.id).catch(() => [] as any[])),
254
+ );
245
255
  let total = 0;
246
- for (const acct of accounts) {
247
- const folders = await getFolders(acct.id);
256
+ for (const folders of folderLists) {
248
257
  const inbox = folders.find((f: any) => f.specialUse === "inbox");
249
258
  if (inbox) total += inbox.unreadCount || 0;
250
259
  }
@@ -718,9 +727,30 @@ async function openCompose(mode: ComposeMode): Promise<void> {
718
727
  console.warn(`[compose] ${mode} — no message selected`);
719
728
  return;
720
729
  }
721
- const accounts = await getAccounts();
722
- const accountId = current?.accountId || accounts[0]?.id || "";
730
+ // Parallel-load: kick off getAccounts AND open the iframe in the same
731
+ // tick. The iframe doesn't need the account list until after its editor
732
+ // bootstraps (200-500 ms for TinyMCE, less for Quill); by then the IPC
733
+ // round-trip has resolved. Earlier code awaited getAccounts FIRST,
734
+ // adding the IPC latency to the perceived Ctrl+N → editor-visible time.
735
+ // We post `compose-init-ready` to the iframe once init is in
736
+ // sessionStorage so compose.ts's IIFE can read synchronously without
737
+ // polling.
738
+ const accountsP = getAccounts();
723
739
  const msg = current?.message;
740
+ // Title bar text needs the subject from msg (no IPC dependency) — build
741
+ // it now so the iframe can be opened with the final title and avoid a
742
+ // flash of placeholder text.
743
+ const titlePrefix =
744
+ mode === "reply" ? "Reply" :
745
+ mode === "replyAll" ? "Reply All" :
746
+ mode === "forward" ? "Forward" :
747
+ "Compose";
748
+ const titleSubject = mode === "new" ? "" : (msg?.subject || "");
749
+ const frame = showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
750
+ // Now finish initialisation off the critical path — editor bootstrap
751
+ // inside the iframe runs concurrently with this await.
752
+ const accounts = await accountsP;
753
+ const accountId = current?.accountId || accounts[0]?.id || "";
724
754
  const rePrefix = /^(re|fwd?):\s*/i;
725
755
  const cleanSubject = msg ? msg.subject.replace(rePrefix, "") : "";
726
756
 
@@ -733,7 +763,7 @@ async function openCompose(mode: ComposeMode): Promise<void> {
733
763
  bodyHtml: "",
734
764
  inReplyTo: "",
735
765
  references: [],
736
- accounts: accounts.map((a: any) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
766
+ accounts: accounts.map((a: any) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
737
767
  };
738
768
 
739
769
  // Auto-detect reply From: if the message was delivered to an identity address
@@ -806,23 +836,16 @@ async function openCompose(mode: ComposeMode): Promise<void> {
806
836
  }
807
837
 
808
838
 
809
- // Store init data for compose window to pick up
839
+ // Store init data for compose window to pick up. sessionStorage is the
840
+ // canonical handoff path — same origin between parent and iframe; the
841
+ // compose IIFE reads it after the editor finishes booting. We also
842
+ // postMessage the iframe so it can short-circuit the listen-for-message
843
+ // wait if it's already past editor init.
810
844
  sessionStorage.setItem("composeInit", JSON.stringify(init));
811
- // Inline compose: load compose.html in an overlay iframe (same origin, same IPC)
812
- // Popup windows don't work in IPC mode (custom protocol doesn't propagate to child windows)
813
- // Title reflects mode + subject so the user can see what they're replying to
814
- // ("Re: Stars of STEM 2026" instead of just "Compose"). Forward shows the
815
- // forward target subject; new compose stays generic.
816
- const titlePrefix =
817
- mode === "reply" ? "Reply" :
818
- mode === "replyAll" ? "Reply All" :
819
- mode === "forward" ? "Forward" :
820
- "Compose";
821
- const titleSubject = mode === "new" ? "" : (msg?.subject || init.subject || "");
822
- showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
845
+ try { frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*"); } catch { /* */ }
823
846
  }
824
847
 
825
- function showComposeOverlay(title = "Compose"): void {
848
+ function showComposeOverlay(title = "Compose"): HTMLIFrameElement {
826
849
  const wrapper = document.createElement("div");
827
850
  wrapper.className = "compose-overlay";
828
851
  // Full-screen on small/short screens, floating on larger
@@ -972,6 +995,7 @@ function showComposeOverlay(title = "Compose"): void {
972
995
  wrapper.appendChild(frame);
973
996
  if (!isSmall) addComposeResizeHandles(wrapper, frame);
974
997
  document.body.appendChild(wrapper);
998
+ return frame;
975
999
  }
976
1000
 
977
1001
  /** Drop a transparent full-viewport shield in front of every other element
@@ -2525,7 +2549,12 @@ async function openComposeFromMailto(m: {
2525
2549
  to: string[]; cc: string[]; bcc: string[];
2526
2550
  subject: string; body: string; inReplyTo: string;
2527
2551
  }): Promise<void> {
2528
- const accounts = await getAccounts();
2552
+ // Open the iframe immediately and load accounts in parallel — same
2553
+ // pattern as openCompose. The mailto handler should never feel like
2554
+ // "waiting for the system" to a user who clicked a link.
2555
+ const accountsP = getAccounts();
2556
+ const frame = showComposeOverlay(m.subject ? `Compose: ${m.subject}` : "Compose");
2557
+ const accounts = await accountsP;
2529
2558
  const accountId = accounts[0]?.id || "";
2530
2559
  const escape = (s: string) => s.replace(/[&<>]/g, c =>
2531
2560
  ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]!));
@@ -2542,10 +2571,10 @@ async function openComposeFromMailto(m: {
2542
2571
  bodyHtml,
2543
2572
  inReplyTo: m.inReplyTo,
2544
2573
  references: m.inReplyTo ? [m.inReplyTo] : [],
2545
- accounts: accounts.map((a: any) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
2574
+ accounts: accounts.map((a: any) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
2546
2575
  };
2547
2576
  sessionStorage.setItem("composeInit", JSON.stringify(init));
2548
- showComposeOverlay(m.subject ? `Compose: ${m.subject}` : "Compose");
2577
+ try { frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*"); } catch { /* */ }
2549
2578
  }
2550
2579
 
2551
2580
  // ── Keyboard shortcuts ──
@@ -4096,16 +4125,20 @@ function renderOutboxStatus(s: any): void {
4096
4125
  setInterval(async () => {
4097
4126
  try {
4098
4127
  const { getOutboxStatus, getDiagnostics } = await import("./lib/api-client.js");
4099
- renderOutboxStatus(await getOutboxStatus());
4100
- renderDiagnosticsBadge(await getDiagnostics());
4128
+ // Run in parallel — neither call depends on the other and each is a
4129
+ // separate IPC round-trip. Earlier code awaited them serially.
4130
+ const [outbox, diag] = await Promise.all([getOutboxStatus(), getDiagnostics()]);
4131
+ renderOutboxStatus(outbox);
4132
+ renderDiagnosticsBadge(diag);
4101
4133
  } catch { /* service unreachable */ }
4102
4134
  }, 15000);
4103
4135
  // First read on startup so the bar isn't blank.
4104
4136
  (async () => {
4105
4137
  try {
4106
4138
  const { getOutboxStatus, getDiagnostics } = await import("./lib/api-client.js");
4107
- renderOutboxStatus(await getOutboxStatus());
4108
- renderDiagnosticsBadge(await getDiagnostics());
4139
+ const [outbox, diag] = await Promise.all([getOutboxStatus(), getDiagnostics()]);
4140
+ renderOutboxStatus(outbox);
4141
+ renderDiagnosticsBadge(diag);
4109
4142
  } catch { /* */ }
4110
4143
  })();
4111
4144
 
@@ -2962,6 +2962,15 @@ document.addEventListener("contextmenu", () => {
2962
2962
 
2963
2963
  // client/compose/compose.ts
2964
2964
  logClientEvent("compose-module-loaded", { href: location.href, version: window.mailxVersion || "?" });
2965
+ var _composeT0 = performance.now();
2966
+ function _ctick(label) {
2967
+ const ms = (performance.now() - _composeT0).toFixed(0).padStart(5);
2968
+ try {
2969
+ logClientEvent(`compose-tick ${ms}ms ${label}`);
2970
+ } catch {
2971
+ }
2972
+ }
2973
+ _ctick("module body executing");
2965
2974
  function closeCompose() {
2966
2975
  logClientEvent("compose-close");
2967
2976
  try {
@@ -3000,8 +3009,8 @@ async function loadEditorAssets(type) {
3000
3009
  loadScript(`${cdn}/@tiptap/extension-placeholder@2/dist/index.umd.js`)
3001
3010
  ]);
3002
3011
  } else {
3003
- loadCSS("https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css");
3004
- await loadScript("https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js");
3012
+ loadCSS("../lib/quill/quill.snow.css");
3013
+ await loadScript("../lib/quill/quill.js");
3005
3014
  }
3006
3015
  }
3007
3016
  var editorType = "quill";
@@ -3083,7 +3092,9 @@ async function tryEditor(type) {
3083
3092
  }
3084
3093
  }
3085
3094
  var activeEditorType = editorType;
3095
+ _ctick(`editor load start (${editorType})`);
3086
3096
  editor = await tryEditor(editorType);
3097
+ _ctick(`editor load end (${editorType}, ok=${!!editor})`);
3087
3098
  if (!editor) {
3088
3099
  const fallbackType = editorType === "quill" ? "tiptap" : "quill";
3089
3100
  logClientEvent("compose-editor-fallback-other", { from: editorType, to: fallbackType });
@@ -3668,13 +3679,41 @@ function scheduleDraftSave() {
3668
3679
  });
3669
3680
  }, DRAFT_INPUT_DEBOUNCE_MS);
3670
3681
  }
3682
+ var _parentInitReady = !!sessionStorage.getItem("composeInit");
3683
+ var _parentInitListeners = [];
3684
+ window.addEventListener("message", (e) => {
3685
+ if (e.data?.type !== "compose-init-ready") return;
3686
+ _parentInitReady = true;
3687
+ for (const fn of _parentInitListeners.splice(0)) fn();
3688
+ });
3689
+ function waitForParentInit(maxMs) {
3690
+ if (_parentInitReady) return Promise.resolve();
3691
+ return new Promise((resolve) => {
3692
+ const timer = setTimeout(() => {
3693
+ _parentInitReady = true;
3694
+ resolve();
3695
+ }, maxMs);
3696
+ _parentInitListeners.push(() => {
3697
+ clearTimeout(timer);
3698
+ resolve();
3699
+ });
3700
+ });
3701
+ }
3671
3702
  (async () => {
3703
+ _ctick("init IIFE start");
3704
+ if (!sessionStorage.getItem("composeInit")) {
3705
+ _ctick("waiting for parent init");
3706
+ await waitForParentInit(1500);
3707
+ _ctick("parent init received");
3708
+ }
3672
3709
  const stored = sessionStorage.getItem("composeInit");
3673
3710
  if (stored) {
3674
3711
  sessionStorage.removeItem("composeInit");
3675
3712
  const init = JSON.parse(stored);
3713
+ _ctick(`init parsed (mode=${init.mode}, bodyHtml=${init.bodyHtml?.length || 0} bytes)`);
3676
3714
  if (init.accounts && init.accounts.length > 0) {
3677
3715
  applyInit(init);
3716
+ _ctick("applyInit done \u2014 compose visible");
3678
3717
  getAccounts().then((fresh) => {
3679
3718
  if (Array.isArray(fresh) && fresh.length > 0) {
3680
3719
  init.accounts = fresh;