@bobfrankston/mailx 1.0.442 → 1.0.443
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.js +11 -3
- package/client/compose/compose.js +51 -2
- package/package.json +1 -1
- package/packages/mailx-service/index.js +15 -3
package/client/app.js
CHANGED
|
@@ -696,9 +696,17 @@ async function openCompose(mode) {
|
|
|
696
696
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
697
697
|
// Inline compose: load compose.html in an overlay iframe (same origin, same IPC)
|
|
698
698
|
// Popup windows don't work in IPC mode (custom protocol doesn't propagate to child windows)
|
|
699
|
-
|
|
699
|
+
// Title reflects mode + subject so the user can see what they're replying to
|
|
700
|
+
// ("Re: Stars of STEM 2026" instead of just "Compose"). Forward shows the
|
|
701
|
+
// forward target subject; new compose stays generic.
|
|
702
|
+
const titlePrefix = mode === "reply" ? "Reply" :
|
|
703
|
+
mode === "replyAll" ? "Reply All" :
|
|
704
|
+
mode === "forward" ? "Forward" :
|
|
705
|
+
"Compose";
|
|
706
|
+
const titleSubject = mode === "new" ? "" : (msg?.subject || init.subject || "");
|
|
707
|
+
showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
|
|
700
708
|
}
|
|
701
|
-
function showComposeOverlay() {
|
|
709
|
+
function showComposeOverlay(title = "Compose") {
|
|
702
710
|
const wrapper = document.createElement("div");
|
|
703
711
|
wrapper.className = "compose-overlay";
|
|
704
712
|
// Full-screen on small/short screens, floating on larger
|
|
@@ -712,7 +720,7 @@ function showComposeOverlay() {
|
|
|
712
720
|
// Title bar — drag to move, close button
|
|
713
721
|
const titleBar = document.createElement("div");
|
|
714
722
|
titleBar.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:#e8ecf0;border-radius:8px 8px 0 0;cursor:move;user-select:none;flex-shrink:0;";
|
|
715
|
-
titleBar.textContent =
|
|
723
|
+
titleBar.textContent = title;
|
|
716
724
|
const closeBtn = document.createElement("button");
|
|
717
725
|
closeBtn.textContent = "✕";
|
|
718
726
|
closeBtn.title = "Save draft and close";
|
|
@@ -92,10 +92,59 @@ catch { /* private-mode / SecurityError — default quill */ }
|
|
|
92
92
|
}
|
|
93
93
|
catch { /* non-fatal */ }
|
|
94
94
|
})();
|
|
95
|
-
|
|
95
|
+
// Whatever happens in editor init, surface failures to the mailx log.
|
|
96
|
+
// Until this round the failures were swallowed silently — Quill CDN
|
|
97
|
+
// timing out or createEditor throwing left the iframe with empty body
|
|
98
|
+
// + dead buttons (because the rest of the script after the await never
|
|
99
|
+
// ran). With logClientEvent here, the failure mode is visible in the
|
|
100
|
+
// service log instead of "Reply just shows blank compose".
|
|
101
|
+
let editor;
|
|
102
|
+
try {
|
|
103
|
+
await loadEditorAssets(editorType);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
logClientEvent("compose-editor-assets-failed", { type: editorType, error: String(e?.message || e) });
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
96
109
|
const container = document.getElementById("compose-editor");
|
|
97
110
|
container.classList.add(editorType === "tiptap" ? "editor-tiptap" : "editor-quill");
|
|
98
|
-
|
|
111
|
+
try {
|
|
112
|
+
editor = await createEditor(container, editorType);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
logClientEvent("compose-editor-create-failed", { type: editorType, error: String(e?.message || e) });
|
|
116
|
+
// Render a minimal contenteditable fallback so the user can still type
|
|
117
|
+
// SOMETHING. Without this, an editor failure leaves the compose form
|
|
118
|
+
// half-functional (To/Cc/Bcc work, body doesn't) and the user doesn't
|
|
119
|
+
// know why. The fallback is a plain div — no toolbar, no rich text.
|
|
120
|
+
container.innerHTML = `<div class="compose-fallback-editor" contenteditable="true" style="border:1px solid #c00;padding:8px;min-height:200px;background:#fff" data-fallback="true"></div>`;
|
|
121
|
+
const fallback = container.querySelector(".compose-fallback-editor");
|
|
122
|
+
editor = {
|
|
123
|
+
root: fallback,
|
|
124
|
+
setHtml: (html) => { fallback.innerHTML = html; },
|
|
125
|
+
getHtml: () => fallback.innerHTML,
|
|
126
|
+
getText: () => fallback.innerText,
|
|
127
|
+
focus: () => fallback.focus(),
|
|
128
|
+
setCursor: () => { },
|
|
129
|
+
getScrollContainer: () => fallback,
|
|
130
|
+
onContentChange: (handler) => { fallback.addEventListener("input", handler); },
|
|
131
|
+
onKeyDown: (handler) => { fallback.addEventListener("keydown", handler); },
|
|
132
|
+
insertTextAtCursor: (text) => {
|
|
133
|
+
const sel = window.getSelection();
|
|
134
|
+
if (sel && sel.rangeCount > 0) {
|
|
135
|
+
const range = sel.getRangeAt(0);
|
|
136
|
+
range.deleteContents();
|
|
137
|
+
range.insertNode(document.createTextNode(text));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
fallback.append(document.createTextNode(text));
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
// Surface the failure to the user in the status bar so they know
|
|
145
|
+
// why the toolbar is missing and the editor is plain.
|
|
146
|
+
setTimeout(() => showDraftStatus(`Editor failed to load (${editorType}). Plain-text fallback in use. Open log for details.`, true), 0);
|
|
147
|
+
}
|
|
99
148
|
// Ctrl+scroll / Ctrl+= / Ctrl+- / Ctrl+0 zoom for the compose editor body.
|
|
100
149
|
// Persists per-session in localStorage so zoom survives window pop/close cycles.
|
|
101
150
|
(() => {
|
package/package.json
CHANGED
|
@@ -559,11 +559,23 @@ export class MailxService {
|
|
|
559
559
|
// in PATH) fall back to the OS default handler so the user still gets
|
|
560
560
|
// *some* editor. Report which one ran so the UI can say "Opening in
|
|
561
561
|
// Word…" vs "Opening in default editor…".
|
|
562
|
-
|
|
562
|
+
//
|
|
563
|
+
// CRITICAL: must use async spawn (not spawnSync). spawnSync blocks
|
|
564
|
+
// the Node event loop until the spawned process exits — and on
|
|
565
|
+
// Windows, `cmd /c start ... <gui-app>` sometimes does not return
|
|
566
|
+
// immediately when the GUI app hangs around. That froze the entire
|
|
567
|
+
// mailx IPC bridge on Edit-in-Word click; subsequent clicks
|
|
568
|
+
// (Discard, X, anything) hung waiting for a response that never
|
|
569
|
+
// came back. Async spawn launches and returns immediately;
|
|
570
|
+
// success/failure of the GUI launch is invisible from here, but
|
|
571
|
+
// the file is written and the watcher is armed regardless.
|
|
572
|
+
const { spawn } = await import("node:child_process");
|
|
563
573
|
const tryLaunch = (cmd, args) => {
|
|
564
574
|
try {
|
|
565
|
-
const
|
|
566
|
-
|
|
575
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
576
|
+
child.on("error", () => { });
|
|
577
|
+
child.unref();
|
|
578
|
+
return true;
|
|
567
579
|
}
|
|
568
580
|
catch {
|
|
569
581
|
return false;
|