@bobfrankston/rmfmail 1.0.679 → 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 +67 -26
- package/client/app.js.map +1 -1
- package/client/app.ts +67 -28
- package/client/compose/compose.bundle.js +62 -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/client/lib/rmf-tiny.js +21 -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.js
CHANGED
|
@@ -174,9 +174,13 @@ function updateBadge(count) {
|
|
|
174
174
|
async function updateNewMessageCount() {
|
|
175
175
|
try {
|
|
176
176
|
const accounts = await getAccounts();
|
|
177
|
+
// Fan out folder queries in parallel — earlier code awaited each
|
|
178
|
+
// account's `getFolders` in series, so an N-account setup paid N
|
|
179
|
+
// back-to-back IPC round-trips on every count refresh (folderCountsChanged,
|
|
180
|
+
// sync events, IDLE updates).
|
|
181
|
+
const folderLists = await Promise.all(accounts.map((acct) => getFolders(acct.id).catch(() => [])));
|
|
177
182
|
let totalUnread = 0;
|
|
178
|
-
for (const
|
|
179
|
-
const folders = await getFolders(acct.id);
|
|
183
|
+
for (const folders of folderLists) {
|
|
180
184
|
const inbox = folders.find((f) => f.specialUse === "inbox");
|
|
181
185
|
if (inbox)
|
|
182
186
|
totalUnread += inbox.unreadCount || 0;
|
|
@@ -252,9 +256,10 @@ window.addEventListener("focus", stopTitleFlash);
|
|
|
252
256
|
/** Call when user actively views messages — resets the badge */
|
|
253
257
|
function markAsSeen() {
|
|
254
258
|
getAccounts().then(async (accounts) => {
|
|
259
|
+
// Parallel folder fetch — see updateNewMessageCount for rationale.
|
|
260
|
+
const folderLists = await Promise.all(accounts.map((acct) => getFolders(acct.id).catch(() => [])));
|
|
255
261
|
let total = 0;
|
|
256
|
-
for (const
|
|
257
|
-
const folders = await getFolders(acct.id);
|
|
262
|
+
for (const folders of folderLists) {
|
|
258
263
|
const inbox = folders.find((f) => f.specialUse === "inbox");
|
|
259
264
|
if (inbox)
|
|
260
265
|
total += inbox.unreadCount || 0;
|
|
@@ -750,9 +755,29 @@ async function openCompose(mode) {
|
|
|
750
755
|
console.warn(`[compose] ${mode} — no message selected`);
|
|
751
756
|
return;
|
|
752
757
|
}
|
|
753
|
-
|
|
754
|
-
|
|
758
|
+
// Parallel-load: kick off getAccounts AND open the iframe in the same
|
|
759
|
+
// tick. The iframe doesn't need the account list until after its editor
|
|
760
|
+
// bootstraps (200-500 ms for TinyMCE, less for Quill); by then the IPC
|
|
761
|
+
// round-trip has resolved. Earlier code awaited getAccounts FIRST,
|
|
762
|
+
// adding the IPC latency to the perceived Ctrl+N → editor-visible time.
|
|
763
|
+
// We post `compose-init-ready` to the iframe once init is in
|
|
764
|
+
// sessionStorage so compose.ts's IIFE can read synchronously without
|
|
765
|
+
// polling.
|
|
766
|
+
const accountsP = getAccounts();
|
|
755
767
|
const msg = current?.message;
|
|
768
|
+
// Title bar text needs the subject from msg (no IPC dependency) — build
|
|
769
|
+
// it now so the iframe can be opened with the final title and avoid a
|
|
770
|
+
// flash of placeholder text.
|
|
771
|
+
const titlePrefix = mode === "reply" ? "Reply" :
|
|
772
|
+
mode === "replyAll" ? "Reply All" :
|
|
773
|
+
mode === "forward" ? "Forward" :
|
|
774
|
+
"Compose";
|
|
775
|
+
const titleSubject = mode === "new" ? "" : (msg?.subject || "");
|
|
776
|
+
const frame = showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
|
|
777
|
+
// Now finish initialisation off the critical path — editor bootstrap
|
|
778
|
+
// inside the iframe runs concurrently with this await.
|
|
779
|
+
const accounts = await accountsP;
|
|
780
|
+
const accountId = current?.accountId || accounts[0]?.id || "";
|
|
756
781
|
const rePrefix = /^(re|fwd?):\s*/i;
|
|
757
782
|
const cleanSubject = msg ? msg.subject.replace(rePrefix, "") : "";
|
|
758
783
|
const init = {
|
|
@@ -764,7 +789,7 @@ async function openCompose(mode) {
|
|
|
764
789
|
bodyHtml: "",
|
|
765
790
|
inReplyTo: "",
|
|
766
791
|
references: [],
|
|
767
|
-
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
|
|
792
|
+
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
|
|
768
793
|
};
|
|
769
794
|
// Auto-detect reply From: if the message was delivered to an identity address
|
|
770
795
|
// (an alias on the account's domain, or the explicit `identityDomains` list
|
|
@@ -838,19 +863,16 @@ async function openCompose(mode) {
|
|
|
838
863
|
init.bodyHtml = forwardBody(msg);
|
|
839
864
|
init.fromAddress = detectReplyFrom();
|
|
840
865
|
}
|
|
841
|
-
// Store init data for compose window to pick up
|
|
866
|
+
// Store init data for compose window to pick up. sessionStorage is the
|
|
867
|
+
// canonical handoff path — same origin between parent and iframe; the
|
|
868
|
+
// compose IIFE reads it after the editor finishes booting. We also
|
|
869
|
+
// postMessage the iframe so it can short-circuit the listen-for-message
|
|
870
|
+
// wait if it's already past editor init.
|
|
842
871
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
// forward target subject; new compose stays generic.
|
|
848
|
-
const titlePrefix = mode === "reply" ? "Reply" :
|
|
849
|
-
mode === "replyAll" ? "Reply All" :
|
|
850
|
-
mode === "forward" ? "Forward" :
|
|
851
|
-
"Compose";
|
|
852
|
-
const titleSubject = mode === "new" ? "" : (msg?.subject || init.subject || "");
|
|
853
|
-
showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
|
|
872
|
+
try {
|
|
873
|
+
frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*");
|
|
874
|
+
}
|
|
875
|
+
catch { /* */ }
|
|
854
876
|
}
|
|
855
877
|
function showComposeOverlay(title = "Compose") {
|
|
856
878
|
const wrapper = document.createElement("div");
|
|
@@ -1000,6 +1022,7 @@ function showComposeOverlay(title = "Compose") {
|
|
|
1000
1022
|
if (!isSmall)
|
|
1001
1023
|
addComposeResizeHandles(wrapper, frame);
|
|
1002
1024
|
document.body.appendChild(wrapper);
|
|
1025
|
+
return frame;
|
|
1003
1026
|
}
|
|
1004
1027
|
/** Drop a transparent full-viewport shield in front of every other element
|
|
1005
1028
|
* so mousemove events stay in the document during a drag. Setting
|
|
@@ -1104,6 +1127,12 @@ function sanitizeQuotedBody(msg) {
|
|
|
1104
1127
|
// future provider/path that didn't go through that pipeline.
|
|
1105
1128
|
const isPlainText = !msg.bodyHtml;
|
|
1106
1129
|
if (isPlainText) {
|
|
1130
|
+
// Full HTML escape. Leaving `>` unescaped was tempting for source
|
|
1131
|
+
// readability but breaks HTML in edge cases — TinyMCE's normalize-
|
|
1132
|
+
// on-paste re-interprets the input, and stray `>` near sequences
|
|
1133
|
+
// like `<!--` / `-->` / `<!` in plain-text bodies can be misread
|
|
1134
|
+
// by the parser. Per Bob 2026-05-12: "not just ugly, it breaks
|
|
1135
|
+
// the HTML." Trivial source-clutter is the lesser evil.
|
|
1107
1136
|
const escaped = String(msg.bodyText || "")
|
|
1108
1137
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1109
1138
|
return `<div style="white-space:pre-wrap;font-family:inherit;margin:0">${escaped}</div>`;
|
|
@@ -2640,7 +2669,12 @@ onWsEvent((event) => {
|
|
|
2640
2669
|
* rather than a single line, escaping `<` so embedded angle brackets in a
|
|
2641
2670
|
* signature/template don't get interpreted as tags. */
|
|
2642
2671
|
async function openComposeFromMailto(m) {
|
|
2643
|
-
|
|
2672
|
+
// Open the iframe immediately and load accounts in parallel — same
|
|
2673
|
+
// pattern as openCompose. The mailto handler should never feel like
|
|
2674
|
+
// "waiting for the system" to a user who clicked a link.
|
|
2675
|
+
const accountsP = getAccounts();
|
|
2676
|
+
const frame = showComposeOverlay(m.subject ? `Compose: ${m.subject}` : "Compose");
|
|
2677
|
+
const accounts = await accountsP;
|
|
2644
2678
|
const accountId = accounts[0]?.id || "";
|
|
2645
2679
|
const escape = (s) => s.replace(/[&<>]/g, c => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
|
2646
2680
|
const bodyHtml = m.body
|
|
@@ -2656,10 +2690,13 @@ async function openComposeFromMailto(m) {
|
|
|
2656
2690
|
bodyHtml,
|
|
2657
2691
|
inReplyTo: m.inReplyTo,
|
|
2658
2692
|
references: m.inReplyTo ? [m.inReplyTo] : [],
|
|
2659
|
-
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
|
|
2693
|
+
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
|
|
2660
2694
|
};
|
|
2661
2695
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
2662
|
-
|
|
2696
|
+
try {
|
|
2697
|
+
frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*");
|
|
2698
|
+
}
|
|
2699
|
+
catch { /* */ }
|
|
2663
2700
|
}
|
|
2664
2701
|
// ── Keyboard shortcuts ──
|
|
2665
2702
|
// Capture-phase pre-handler: intercept WebView accelerator keys that would
|
|
@@ -4389,8 +4426,11 @@ function renderOutboxStatus(s) {
|
|
|
4389
4426
|
setInterval(async () => {
|
|
4390
4427
|
try {
|
|
4391
4428
|
const { getOutboxStatus, getDiagnostics } = await import("./lib/api-client.js");
|
|
4392
|
-
|
|
4393
|
-
|
|
4429
|
+
// Run in parallel — neither call depends on the other and each is a
|
|
4430
|
+
// separate IPC round-trip. Earlier code awaited them serially.
|
|
4431
|
+
const [outbox, diag] = await Promise.all([getOutboxStatus(), getDiagnostics()]);
|
|
4432
|
+
renderOutboxStatus(outbox);
|
|
4433
|
+
renderDiagnosticsBadge(diag);
|
|
4394
4434
|
}
|
|
4395
4435
|
catch { /* service unreachable */ }
|
|
4396
4436
|
}, 15000);
|
|
@@ -4398,8 +4438,9 @@ setInterval(async () => {
|
|
|
4398
4438
|
(async () => {
|
|
4399
4439
|
try {
|
|
4400
4440
|
const { getOutboxStatus, getDiagnostics } = await import("./lib/api-client.js");
|
|
4401
|
-
|
|
4402
|
-
|
|
4441
|
+
const [outbox, diag] = await Promise.all([getOutboxStatus(), getDiagnostics()]);
|
|
4442
|
+
renderOutboxStatus(outbox);
|
|
4443
|
+
renderDiagnosticsBadge(diag);
|
|
4403
4444
|
}
|
|
4404
4445
|
catch { /* */ }
|
|
4405
4446
|
})();
|