@bobfrankston/rmfmail 1.0.680 → 1.0.686
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/bin/lean-accounts.js +0 -1
- package/client/app.bundle.js +172 -55
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +73 -27
- package/client/app.js.map +1 -1
- package/client/app.ts +73 -29
- package/client/components/context-menu.js +2 -0
- package/client/components/context-menu.js.map +1 -1
- package/client/components/context-menu.ts +6 -0
- package/client/components/folder-tree.js +26 -4
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +21 -4
- package/client/components/message-list.js +108 -40
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +103 -38
- package/client/compose/compose.bundle.js +189 -17
- package/client/compose/compose.bundle.js.map +3 -3
- package/client/compose/compose.js +51 -3
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +47 -3
- package/client/compose/spellcheck.js +178 -12
- package/client/compose/spellcheck.js.map +1 -1
- package/client/compose/spellcheck.ts +168 -8
- package/client/lib/api-client.js +3 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +4 -0
- package/client/lib/mailxapi.js +3 -0
- package/client/lib/quill/quill.js +3 -0
- package/client/lib/quill/quill.snow.css +10 -0
- package/client/lib/rmf-tiny.js +25 -6
- package/docs/accounts.md +7 -2
- package/package.json +8 -8
- package/packages/mailx-core/index.d.ts.map +1 -1
- package/packages/mailx-core/index.js +2 -12
- package/packages/mailx-core/index.js.map +1 -1
- package/packages/mailx-core/index.ts +2 -12
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +31 -6
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +32 -6
- package/packages/mailx-imap/node_modules.npmglobalize-stash-11884/.package-lock.json +116 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +22 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +134 -6
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +128 -11
- package/packages/mailx-service/jsonrpc.js +3 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +3 -0
- package/packages/mailx-service/local-store.d.ts.map +1 -1
- package/packages/mailx-service/local-store.js +15 -12
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/local-store.ts +15 -12
- package/packages/mailx-settings/docs/accounts.md +14 -1
- package/packages/mailx-settings/docs/npmglobalize-disttag.md +90 -0
- package/packages/mailx-settings/docs/prod-android.md +88 -0
- package/packages/mailx-settings/docs/prod.md +224 -0
- package/packages/mailx-settings/docs/push-relay.md +141 -0
- package/packages/mailx-settings/docs/rmf-tiny.md +156 -0
- package/packages/mailx-settings/index.d.ts +2 -2
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +13 -10
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +13 -9
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +44 -6
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +47 -6
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/package.json +4 -1
- package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
- package/packages/mailx-store-web/web-settings.js +0 -1
- package/packages/mailx-store-web/web-settings.js.map +1 -1
- package/packages/mailx-store-web/web-settings.ts +0 -1
- package/packages/mailx-types/index.d.ts +1 -2
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +1 -2
- package/packages/mailx-types/package.json +1 -1
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
|
|
@@ -1110,8 +1133,19 @@ function sanitizeQuotedBody(msg) {
|
|
|
1110
1133
|
// like `<!--` / `-->` / `<!` in plain-text bodies can be misread
|
|
1111
1134
|
// by the parser. Per Bob 2026-05-12: "not just ugly, it breaks
|
|
1112
1135
|
// the HTML." Trivial source-clutter is the lesser evil.
|
|
1136
|
+
//
|
|
1137
|
+
// CRLF → <br>. `white-space:pre-wrap` alone is not enough: TinyMCE
|
|
1138
|
+
// (and other HTML editors) normalize text-node whitespace when
|
|
1139
|
+
// setContent ingests the HTML, collapsing `\n` to spaces BEFORE
|
|
1140
|
+
// CSS runs. The literal `<br>` survives the normalization, so we
|
|
1141
|
+
// get one line break per source line whether the editor preserves
|
|
1142
|
+
// raw whitespace or not. pre-wrap stays as belt-and-braces and to
|
|
1143
|
+
// keep multi-space runs (alignment, indented quote-markers like
|
|
1144
|
+
// `> > >`) visible.
|
|
1113
1145
|
const escaped = String(msg.bodyText || "")
|
|
1114
|
-
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
1146
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
1147
|
+
.replace(/\r\n?/g, "\n")
|
|
1148
|
+
.replace(/\n/g, "<br>");
|
|
1115
1149
|
return `<div style="white-space:pre-wrap;font-family:inherit;margin:0">${escaped}</div>`;
|
|
1116
1150
|
}
|
|
1117
1151
|
let body = msg.bodyHtml;
|
|
@@ -2646,7 +2680,12 @@ onWsEvent((event) => {
|
|
|
2646
2680
|
* rather than a single line, escaping `<` so embedded angle brackets in a
|
|
2647
2681
|
* signature/template don't get interpreted as tags. */
|
|
2648
2682
|
async function openComposeFromMailto(m) {
|
|
2649
|
-
|
|
2683
|
+
// Open the iframe immediately and load accounts in parallel — same
|
|
2684
|
+
// pattern as openCompose. The mailto handler should never feel like
|
|
2685
|
+
// "waiting for the system" to a user who clicked a link.
|
|
2686
|
+
const accountsP = getAccounts();
|
|
2687
|
+
const frame = showComposeOverlay(m.subject ? `Compose: ${m.subject}` : "Compose");
|
|
2688
|
+
const accounts = await accountsP;
|
|
2650
2689
|
const accountId = accounts[0]?.id || "";
|
|
2651
2690
|
const escape = (s) => s.replace(/[&<>]/g, c => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
|
2652
2691
|
const bodyHtml = m.body
|
|
@@ -2662,10 +2701,13 @@ async function openComposeFromMailto(m) {
|
|
|
2662
2701
|
bodyHtml,
|
|
2663
2702
|
inReplyTo: m.inReplyTo,
|
|
2664
2703
|
references: m.inReplyTo ? [m.inReplyTo] : [],
|
|
2665
|
-
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
|
|
2704
|
+
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
|
|
2666
2705
|
};
|
|
2667
2706
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
2668
|
-
|
|
2707
|
+
try {
|
|
2708
|
+
frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*");
|
|
2709
|
+
}
|
|
2710
|
+
catch { /* */ }
|
|
2669
2711
|
}
|
|
2670
2712
|
// ── Keyboard shortcuts ──
|
|
2671
2713
|
// Capture-phase pre-handler: intercept WebView accelerator keys that would
|
|
@@ -4395,8 +4437,11 @@ function renderOutboxStatus(s) {
|
|
|
4395
4437
|
setInterval(async () => {
|
|
4396
4438
|
try {
|
|
4397
4439
|
const { getOutboxStatus, getDiagnostics } = await import("./lib/api-client.js");
|
|
4398
|
-
|
|
4399
|
-
|
|
4440
|
+
// Run in parallel — neither call depends on the other and each is a
|
|
4441
|
+
// separate IPC round-trip. Earlier code awaited them serially.
|
|
4442
|
+
const [outbox, diag] = await Promise.all([getOutboxStatus(), getDiagnostics()]);
|
|
4443
|
+
renderOutboxStatus(outbox);
|
|
4444
|
+
renderDiagnosticsBadge(diag);
|
|
4400
4445
|
}
|
|
4401
4446
|
catch { /* service unreachable */ }
|
|
4402
4447
|
}, 15000);
|
|
@@ -4404,8 +4449,9 @@ setInterval(async () => {
|
|
|
4404
4449
|
(async () => {
|
|
4405
4450
|
try {
|
|
4406
4451
|
const { getOutboxStatus, getDiagnostics } = await import("./lib/api-client.js");
|
|
4407
|
-
|
|
4408
|
-
|
|
4452
|
+
const [outbox, diag] = await Promise.all([getOutboxStatus(), getDiagnostics()]);
|
|
4453
|
+
renderOutboxStatus(outbox);
|
|
4454
|
+
renderDiagnosticsBadge(diag);
|
|
4409
4455
|
}
|
|
4410
4456
|
catch { /* */ }
|
|
4411
4457
|
})();
|