@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/bin/build-quill.js +35 -0
- package/client/app.bundle.js +34 -17
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +61 -26
- package/client/app.js.map +1 -1
- package/client/app.ts +61 -28
- package/client/compose/compose.bundle.js +41 -2
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +51 -3
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +47 -3
- package/client/lib/quill/quill.js +3 -0
- package/client/lib/quill/quill.snow.css +10 -0
- package/docs/accounts.md +7 -2
- package/package.json +4 -4
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +11 -6
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +19 -11
- package/packages/mailx-service/local-store.d.ts.map +1 -1
- package/packages/mailx-service/local-store.js +9 -0
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/local-store.ts +9 -0
- package/packages/mailx-settings/index.d.ts +1 -1
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +12 -6
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +12 -6
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
|
|
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
|
|
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
|
-
|
|
722
|
-
|
|
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
|
-
|
|
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"):
|
|
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
|
-
|
|
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
|
({ "&": "&", "<": "<", ">": ">" }[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
|
-
|
|
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
|
-
|
|
4100
|
-
|
|
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
|
-
|
|
4108
|
-
|
|
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("
|
|
3004
|
-
await loadScript("
|
|
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;
|