@bobfrankston/rmfmail 1.2.167 → 1.2.169

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.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/TODO.md +1 -0
  3. package/client/app.bundle.js +28 -0
  4. package/client/app.bundle.js.map +2 -2
  5. package/client/app.js +23 -0
  6. package/client/app.js.map +1 -1
  7. package/client/app.ts +18 -0
  8. package/client/compose/compose.bundle.js +292 -0
  9. package/client/compose/compose.bundle.js.map +4 -4
  10. package/client/compose/compose.js +23 -0
  11. package/client/compose/compose.js.map +1 -1
  12. package/client/compose/compose.ts +20 -0
  13. package/client/compose/harper-lint.js +269 -0
  14. package/client/compose/harper-lint.js.map +1 -0
  15. package/client/compose/harper-lint.ts +265 -0
  16. package/client/index.html +1 -0
  17. package/client/lib/api-client.js +8 -0
  18. package/client/lib/api-client.js.map +1 -1
  19. package/client/lib/api-client.ts +8 -0
  20. package/client/lib/mailxapi.js +6 -0
  21. package/docs/harper-integration.md +120 -0
  22. package/package.json +8 -5
  23. package/packages/mailx-imap/package-lock.json +2 -2
  24. package/packages/mailx-imap/package.json +1 -1
  25. package/packages/mailx-service/index.d.ts +11 -0
  26. package/packages/mailx-service/index.d.ts.map +1 -1
  27. package/packages/mailx-service/index.js +60 -0
  28. package/packages/mailx-service/index.js.map +1 -1
  29. package/packages/mailx-service/index.ts +58 -0
  30. package/packages/mailx-service/jsonrpc.js +2 -0
  31. package/packages/mailx-service/jsonrpc.js.map +1 -1
  32. package/packages/mailx-service/jsonrpc.ts +2 -0
  33. package/packages/mailx-service/package.json +1 -1
  34. package/packages/mailx-settings/docs/harper-integration.md +120 -0
  35. package/packages/mailx-settings/package.json +1 -1
  36. package/packages/mailx-store/package.json +1 -1
  37. package/packages/mailx-store-web/package.json +1 -1
  38. package/packages/mailx-types/mailx-api.d.ts +12 -0
  39. package/packages/mailx-types/mailx-api.d.ts.map +1 -1
  40. package/packages/mailx-types/mailx-api.ts +4 -0
  41. package/packages/mailx-types/package.json +1 -1
  42. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-28172 → node_modules.npmglobalize-stash-69656}/.package-lock.json +0 -0
package/client/app.ts CHANGED
@@ -5356,6 +5356,12 @@ getSettings().then((s: any) => {
5356
5356
  const px = Number(s.ui?.composeFontSize);
5357
5357
  fontEl.value = String(Number.isFinite(px) && px >= 8 && px <= 32 ? px : 15);
5358
5358
  }
5359
+ const gcEl = document.getElementById("opt-grammar-check") as HTMLInputElement | null;
5360
+ if (gcEl) {
5361
+ const on = s.ui?.grammarCheck !== false; // default ON
5362
+ gcEl.checked = on;
5363
+ try { localStorage.setItem("mailx-grammar-check", on ? "1" : "0"); } catch { /* */ }
5364
+ }
5359
5365
  }).catch(() => {});
5360
5366
 
5361
5367
  document.getElementById("opt-compose-window")?.addEventListener("change", (e) => {
@@ -5367,6 +5373,18 @@ document.getElementById("opt-compose-window")?.addEventListener("change", (e) =>
5367
5373
  }).catch(() => {});
5368
5374
  });
5369
5375
 
5376
+ // Harper grammar squiggles in compose — same dual persistence as the
5377
+ // compose-window toggle: localStorage for the in-window compose iframe
5378
+ // (same origin), settings.ui for the popout's boot snapshot.
5379
+ document.getElementById("opt-grammar-check")?.addEventListener("change", (e) => {
5380
+ const on = (e.target as HTMLInputElement).checked;
5381
+ try { localStorage.setItem("mailx-grammar-check", on ? "1" : "0"); } catch { /* */ }
5382
+ getSettings().then((settings: any) => {
5383
+ settings.ui = { ...settings.ui, grammarCheck: on };
5384
+ saveSettings(settings);
5385
+ }).catch(() => {});
5386
+ });
5387
+
5370
5388
  // Unified compose editing font size (px). Same sync-cache-then-persist shape
5371
5389
  // as saveEditorSetting: compose.ts reads `mailx-compose-font-size` from
5372
5390
  // localStorage at module load, so the very next compose-open is correct.
@@ -1069,6 +1069,7 @@ __export(api_client_exports, {
1069
1069
  getUnifiedInbox: () => getUnifiedInbox,
1070
1070
  getUserDict: () => getUserDict,
1071
1071
  getVersion: () => getVersion,
1072
+ grammarLint: () => grammarLint,
1072
1073
  hasBccHistoryTo: () => hasBccHistoryTo,
1073
1074
  hasCcHistoryTo: () => hasCcHistoryTo,
1074
1075
  installConsoleCapture: () => installConsoleCapture,
@@ -1629,6 +1630,12 @@ async function openAttachment(accountId, uid, attachmentId, folderId, filename)
1629
1630
  async function getMessageSource(accountId, uid, folderId) {
1630
1631
  return ipc().getMessageSource(accountId, uid, folderId);
1631
1632
  }
1633
+ async function grammarLint(blocks) {
1634
+ const fn = ipc().grammarLint;
1635
+ if (!fn)
1636
+ throw new Error("no grammarLint bridge");
1637
+ return fn(blocks);
1638
+ }
1632
1639
  async function registerMailto() {
1633
1640
  const fn = ipc().registerMailto;
1634
1641
  if (!fn)
@@ -2914,6 +2921,277 @@ var init_spellcheck = __esm({
2914
2921
  }
2915
2922
  });
2916
2923
 
2924
+ // client/compose/harper-lint.js
2925
+ var harper_lint_exports = {};
2926
+ __export(harper_lint_exports, {
2927
+ initHarperLint: () => initHarperLint
2928
+ });
2929
+ function initHarperLint(ed) {
2930
+ let disposed = false;
2931
+ let timer = null;
2932
+ let placed = [];
2933
+ let paintQueued = false;
2934
+ let bridgeDead = false;
2935
+ const target = () => {
2936
+ try {
2937
+ const nat = ed.nativeEditor;
2938
+ if (nat?.getBody) {
2939
+ const b = nat.getBody();
2940
+ return b ? { root: b, doc: b.ownerDocument } : null;
2941
+ }
2942
+ return ed.root ? { root: ed.root, doc: ed.root.ownerDocument } : null;
2943
+ } catch {
2944
+ return null;
2945
+ }
2946
+ };
2947
+ const overlayHost = (doc, root) => root === doc.body ? doc.documentElement : doc.body;
2948
+ const clearOverlay = (doc) => {
2949
+ doc.getElementById(OVERLAY_ID2)?.remove();
2950
+ doc.getElementById(POPUP_ID)?.remove();
2951
+ };
2952
+ const collectBlocks = (root) => {
2953
+ const kids = Array.from(root.children).filter((c) => c instanceof HTMLElement);
2954
+ const blocks = (kids.length ? kids : [root]).filter((b) => b.tagName !== "BLOCKQUOTE" && !b.closest("blockquote") && !b.querySelector("blockquote"));
2955
+ return blocks;
2956
+ };
2957
+ const blockText = (block, doc) => {
2958
+ const walker = doc.createTreeWalker(block, NodeFilter.SHOW_TEXT);
2959
+ const nodes = [];
2960
+ let text = "";
2961
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
2962
+ nodes.push({ node: n, from: text.length });
2963
+ text += n.data;
2964
+ }
2965
+ return { text, nodes };
2966
+ };
2967
+ const spanToRange = (doc, nodes, start, end) => {
2968
+ let a = null;
2969
+ let b = null;
2970
+ for (const e of nodes) {
2971
+ const len = e.node.data.length;
2972
+ if (!a && start >= e.from && start <= e.from + len)
2973
+ a = { node: e.node, off: start - e.from };
2974
+ if (end >= e.from && end <= e.from + len)
2975
+ b = { node: e.node, off: end - e.from };
2976
+ }
2977
+ if (!a || !b)
2978
+ return null;
2979
+ try {
2980
+ const r = doc.createRange();
2981
+ r.setStart(a.node, a.off);
2982
+ r.setEnd(b.node, b.off);
2983
+ return r;
2984
+ } catch {
2985
+ return null;
2986
+ }
2987
+ };
2988
+ const paint = () => {
2989
+ if (disposed)
2990
+ return;
2991
+ const t = target();
2992
+ if (!t)
2993
+ return;
2994
+ clearOverlay(t.doc);
2995
+ if (placed.length === 0)
2996
+ return;
2997
+ const host = overlayHost(t.doc, t.root);
2998
+ const ov = t.doc.createElement("div");
2999
+ ov.id = OVERLAY_ID2;
3000
+ ov.style.cssText = "position:absolute;left:0;top:0;width:0;height:0;overflow:visible;pointer-events:none;z-index:5;";
3001
+ const win = t.doc.defaultView || window;
3002
+ for (const p of placed) {
3003
+ let rects = [];
3004
+ try {
3005
+ rects = p.range.getClientRects();
3006
+ } catch {
3007
+ continue;
3008
+ }
3009
+ for (const r of Array.from(rects)) {
3010
+ if (r.width < 2)
3011
+ continue;
3012
+ const u = t.doc.createElement("div");
3013
+ u.style.cssText = `position:absolute;left:${r.left + win.scrollX}px;top:${r.bottom + win.scrollY - 2}px;width:${r.width}px;height:0;border-bottom:2px dotted rgba(66,133,244,0.9);pointer-events:none;`;
3014
+ ov.appendChild(u);
3015
+ }
3016
+ }
3017
+ host.appendChild(ov);
3018
+ };
3019
+ const queuePaint = () => {
3020
+ if (paintQueued || disposed)
3021
+ return;
3022
+ paintQueued = true;
3023
+ requestAnimationFrame(() => {
3024
+ paintQueued = false;
3025
+ paint();
3026
+ });
3027
+ };
3028
+ const lint = async () => {
3029
+ if (disposed || bridgeDead)
3030
+ return;
3031
+ const t = target();
3032
+ if (!t)
3033
+ return;
3034
+ const blocks = collectBlocks(t.root);
3035
+ const texts = blocks.map((b) => blockText(b, t.doc));
3036
+ let res;
3037
+ try {
3038
+ res = await grammarLint(texts.map((x) => x.text));
3039
+ } catch {
3040
+ bridgeDead = true;
3041
+ clearOverlayAll();
3042
+ return;
3043
+ }
3044
+ if (disposed)
3045
+ return;
3046
+ const next = [];
3047
+ (res?.blocks || []).forEach((lints, i) => {
3048
+ const tx = texts[i];
3049
+ if (!tx)
3050
+ return;
3051
+ for (const l of lints || []) {
3052
+ if (SKIP_KINDS.has(l.kind))
3053
+ continue;
3054
+ const range = spanToRange(t.doc, tx.nodes, l.start, l.end);
3055
+ if (range)
3056
+ next.push({ lint: l, range });
3057
+ }
3058
+ });
3059
+ placed = next;
3060
+ queuePaint();
3061
+ };
3062
+ const clearOverlayAll = () => {
3063
+ const t = target();
3064
+ if (t)
3065
+ clearOverlay(t.doc);
3066
+ placed = [];
3067
+ };
3068
+ const schedule = () => {
3069
+ if (timer)
3070
+ clearTimeout(timer);
3071
+ timer = setTimeout(() => {
3072
+ void lint();
3073
+ }, DEBOUNCE_MS);
3074
+ };
3075
+ const applyFix = (p, replacement, doc) => {
3076
+ try {
3077
+ const nat = ed.nativeEditor;
3078
+ if (nat?.selection?.setRng && nat?.insertContent) {
3079
+ nat.selection.setRng(p.range);
3080
+ const esc = replacement.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3081
+ nat.insertContent(esc);
3082
+ } else {
3083
+ const sel = (doc.defaultView || window).getSelection();
3084
+ if (!sel)
3085
+ return;
3086
+ sel.removeAllRanges();
3087
+ sel.addRange(p.range);
3088
+ doc.execCommand("insertText", false, replacement);
3089
+ }
3090
+ } catch {
3091
+ }
3092
+ doc.getElementById(POPUP_ID)?.remove();
3093
+ schedule();
3094
+ };
3095
+ const showPopup = (p, x, y, doc) => {
3096
+ doc.getElementById(POPUP_ID)?.remove();
3097
+ const pop = doc.createElement("div");
3098
+ pop.id = POPUP_ID;
3099
+ pop.style.cssText = `position:absolute;left:${x}px;top:${y + 6}px;z-index:2100;background:#fff;color:#1a1a1a;border:1px solid #bbb;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.25);padding:6px 0;min-width:180px;max-width:340px;font:13px system-ui,sans-serif;`;
3100
+ const msg = doc.createElement("div");
3101
+ msg.style.cssText = "padding:4px 12px 6px;color:#555;border-bottom:1px solid #eee;";
3102
+ msg.textContent = p.lint.message;
3103
+ pop.appendChild(msg);
3104
+ for (const s of p.lint.suggestions.slice(0, 5)) {
3105
+ const item = doc.createElement("div");
3106
+ item.style.cssText = "padding:6px 12px;cursor:pointer;font-weight:600;";
3107
+ item.textContent = s;
3108
+ item.addEventListener("mouseenter", () => {
3109
+ item.style.background = "#eef3ff";
3110
+ });
3111
+ item.addEventListener("mouseleave", () => {
3112
+ item.style.background = "";
3113
+ });
3114
+ item.addEventListener("mousedown", (e) => {
3115
+ e.preventDefault();
3116
+ e.stopPropagation();
3117
+ applyFix(p, s, doc);
3118
+ });
3119
+ pop.appendChild(item);
3120
+ }
3121
+ if (p.lint.suggestions.length === 0) {
3122
+ const none = doc.createElement("div");
3123
+ none.style.cssText = "padding:6px 12px;color:#888;";
3124
+ none.textContent = "(no suggestion \u2014 reword manually)";
3125
+ pop.appendChild(none);
3126
+ }
3127
+ overlayHost(doc, target()?.root || doc.body).appendChild(pop);
3128
+ const dismiss2 = () => {
3129
+ pop.remove();
3130
+ doc.removeEventListener("mousedown", dismiss2, true);
3131
+ };
3132
+ setTimeout(() => doc.addEventListener("mousedown", dismiss2, true), 0);
3133
+ };
3134
+ const onClick = (e) => {
3135
+ if (disposed || placed.length === 0)
3136
+ return;
3137
+ const t = target();
3138
+ if (!t)
3139
+ return;
3140
+ for (const p of placed) {
3141
+ let rects = [];
3142
+ try {
3143
+ rects = Array.from(p.range.getClientRects());
3144
+ } catch {
3145
+ continue;
3146
+ }
3147
+ for (const r of rects) {
3148
+ if (e.clientX >= r.left - 2 && e.clientX <= r.right + 2 && e.clientY >= r.top - 2 && e.clientY <= r.bottom + 4) {
3149
+ const win = t.doc.defaultView || window;
3150
+ showPopup(p, r.left + win.scrollX, r.bottom + win.scrollY, t.doc);
3151
+ return;
3152
+ }
3153
+ }
3154
+ }
3155
+ };
3156
+ const t0 = target();
3157
+ ed.onContentChange(schedule);
3158
+ if (t0) {
3159
+ t0.doc.addEventListener("click", onClick);
3160
+ t0.doc.addEventListener("scroll", queuePaint, true);
3161
+ (t0.doc.defaultView || window).addEventListener("resize", queuePaint);
3162
+ }
3163
+ try {
3164
+ ed.getScrollContainer()?.addEventListener("scroll", queuePaint);
3165
+ } catch {
3166
+ }
3167
+ setTimeout(() => {
3168
+ void lint();
3169
+ }, 1500);
3170
+ return () => {
3171
+ disposed = true;
3172
+ if (timer)
3173
+ clearTimeout(timer);
3174
+ clearOverlayAll();
3175
+ const t = target();
3176
+ if (t) {
3177
+ t.doc.removeEventListener("click", onClick);
3178
+ t.doc.removeEventListener("scroll", queuePaint, true);
3179
+ (t.doc.defaultView || window).removeEventListener("resize", queuePaint);
3180
+ }
3181
+ };
3182
+ }
3183
+ var DEBOUNCE_MS, OVERLAY_ID2, POPUP_ID, SKIP_KINDS;
3184
+ var init_harper_lint = __esm({
3185
+ "client/compose/harper-lint.js"() {
3186
+ "use strict";
3187
+ init_api_client();
3188
+ DEBOUNCE_MS = 700;
3189
+ OVERLAY_ID2 = "harper-overlay";
3190
+ POPUP_ID = "harper-popup";
3191
+ SKIP_KINDS = /* @__PURE__ */ new Set(["Spelling"]);
3192
+ }
3193
+ });
3194
+
2917
3195
  // client/compose/ghost-text.js
2918
3196
  var ghost_text_exports = {};
2919
3197
  __export(ghost_text_exports, {
@@ -4114,6 +4392,20 @@ async function tryEditor(type) {
4114
4392
  if (native.initialized) attach();
4115
4393
  else native.on("init", attach);
4116
4394
  }
4395
+ try {
4396
+ const bootGrammar = window.__rmfBootSettings?.ui?.grammarCheck;
4397
+ let grammarOn = bootGrammar !== void 0 ? bootGrammar !== false : localStorage.getItem("mailx-grammar-check") !== "0";
4398
+ if (grammarOn && ed) {
4399
+ const attachHarper = () => {
4400
+ Promise.resolve().then(() => (init_harper_lint(), harper_lint_exports)).then((m) => m.initHarperLint(ed)).catch(() => {
4401
+ });
4402
+ };
4403
+ const native = ed.nativeEditor;
4404
+ if (type === "tinymce" && native && !native.initialized) native.on("init", attachHarper);
4405
+ else attachHarper();
4406
+ }
4407
+ } catch {
4408
+ }
4117
4409
  return ed;
4118
4410
  } catch (e) {
4119
4411
  logClientEvent("compose-editor-create-failed", { type, error: String(e?.message || e) });