@bobfrankston/rmfmail 1.2.325 → 1.2.327
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/.commitmsg +8 -18
- package/.llm/config-cache-crash.md +21 -1
- package/client/compose/compose.bundle.js +49 -23
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +45 -4
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +45 -4
- package/client/compose/editor.js +51 -28
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +47 -25
- package/npmchanges.md +22 -0
- package/package.json +5 -5
package/.commitmsg
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
Edit in Word: only the user's own part goes to Word; signature + quoted message are re-attached verbatim
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- mailx-settings 0.1.92: atomicWriteText (tmp + fsync + rename) exported and
|
|
12
|
-
used by every local-cache writer; writeTextIfChanged skips identical
|
|
13
|
-
content; readJsonc reports jsonc-parser errors instead of returning nothing.
|
|
14
|
-
- mailx-imap 0.1.185: pollCloud writes through atomicWriteText.
|
|
15
|
-
- bin/mailx.ts: reconcileAccountsFromDisk — new enabled accounts are added
|
|
16
|
-
live (addAccount + inbox check + sync + IDLE) on any accounts.jsonc change,
|
|
17
|
-
including the boot-time cloud refresh; configChanged carries restartNeeded.
|
|
18
|
-
- client: the Restart banner shows only when restartNeeded; otherwise a
|
|
19
|
-
status-bar line names what changed.
|
|
3
|
+
html-to-docx flattens <blockquote> to plain paragraphs and mammoth brings back
|
|
4
|
+
only <p>/<a>/<br>, so a reply came home with the quoted message's formatting
|
|
5
|
+
gone and the bordered signature reduced to two lines. splitWordEditable()
|
|
6
|
+
(client/compose/compose.ts) cuts the body at the signature rule
|
|
7
|
+
(hr.mailx-sig-rule, legacy hr[style*=3cm]) or div.reply; the head goes to
|
|
8
|
+
Word, the tail is taken from the editor at apply time and appended unchanged.
|
|
9
|
+
Signature rule now carries class="mailx-sig-rule".
|
|
@@ -22,5 +22,25 @@ Session: Claude Fable 5.1, started 2026-09-13 ~17:00 EDT.
|
|
|
22
22
|
4. Bump mailx-settings, mailx-imap; rebuild.cmd.
|
|
23
23
|
|
|
24
24
|
## Status
|
|
25
|
-
- [x] edits - [x] tsc + bundles - [
|
|
25
|
+
- [x] edits - [x] tsc + bundles - [x] rebuild.cmd → rmfmail 1.2.325, settings 0.1.94, imap 0.1.186, APK built (17:10) - [ ] Bob restarts app
|
|
26
26
|
- DONE.md entry written as v1.2.325; .commitmsg written; versions: settings 0.1.92, imap 0.1.185
|
|
27
|
+
|
|
28
|
+
## Follow-on (same session, ~18:45): typing at a link edge extended the link (TinyMCE)
|
|
29
|
+
- Cause: 2026-09-10 step-out ran only after paste; Backspace/click/arrow left the caret in TinyMCE's
|
|
30
|
+
inline-boundary "inside, at end" position (blue highlight) → every keystroke grew the link.
|
|
31
|
+
- Fix: `keepTypingOutsideLinks` in client/compose/editor.ts — check before every printable keydown and
|
|
32
|
+
one tick after paste; moves the caret just after (or before) the anchor when only whitespace/U+FEFF
|
|
33
|
+
separates it from the edge. Quill's typed path NOT covered (not the configured editor).
|
|
34
|
+
- Ships as rmfmail 1.2.326 via rebuild.cmd (running). Bob must restart the daemon for the new bundle.
|
|
35
|
+
|
|
36
|
+
## Follow-on (~19:40): Edit in Word lost the quoted message's formatting
|
|
37
|
+
- Cause: whole body → html-to-docx (flattens blockquote) → Word → mammoth (only p/a/br back).
|
|
38
|
+
- Fix (v1.2.327, publishing): `splitWordEditable()` in client/compose/compose.ts — Word gets only the
|
|
39
|
+
part above the signature rule (`hr.mailx-sig-rule`, legacy `hr[style*=3cm]`) or `div.reply`; the
|
|
40
|
+
tail is re-read from the editor at apply time and appended unchanged.
|
|
41
|
+
- OPEN (Bob's direction): replace mammoth on the return path with his `y:\dev\utils\whts` converter.
|
|
42
|
+
whts today = whole-document CLI (`converter(docPath)` writes files, uses globals/logger/drive
|
|
43
|
+
paths, emits CSS classes via styleToClass, pulls sql/itemgen/pdf deps). Needs a lean exported
|
|
44
|
+
`docxToHtml()` core with inline styles before rmfmail can import it. Not touched — separate project.
|
|
45
|
+
- Checked: bundled TinyMCE 8.8.2 has NO Word-HTML cleaner (that is PowerPaste, paid); only the
|
|
46
|
+
generic paste path and `mceInsertClipboardContent`.
|
|
@@ -5324,32 +5324,43 @@ async function createTinyMceEditor2(container2, opts = {}) {
|
|
|
5324
5324
|
const body = native?.getBody?.();
|
|
5325
5325
|
if (native && body) {
|
|
5326
5326
|
wireMarkdownPaste(body, (html) => native.insertContent(html));
|
|
5327
|
-
|
|
5327
|
+
keepTypingOutsideLinks(native);
|
|
5328
5328
|
}
|
|
5329
5329
|
} catch {
|
|
5330
5330
|
}
|
|
5331
5331
|
return ed;
|
|
5332
5332
|
}
|
|
5333
|
-
function
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5333
|
+
function keepTypingOutsideLinks(native) {
|
|
5334
|
+
const isBlank = (s) => !s.replace(/\uFEFF/g, "").trim();
|
|
5335
|
+
const stepOutOfLinkEdge = () => {
|
|
5336
|
+
try {
|
|
5337
|
+
const rng = native.selection.getRng();
|
|
5338
|
+
if (!rng || !rng.collapsed)
|
|
5339
|
+
return;
|
|
5340
|
+
const node = native.selection.getNode();
|
|
5341
|
+
const a = node?.closest?.("a[href]");
|
|
5342
|
+
if (!a)
|
|
5343
|
+
return;
|
|
5344
|
+
const toEnd = rng.cloneRange();
|
|
5345
|
+
toEnd.setEnd(a, a.childNodes.length);
|
|
5346
|
+
const fromStart = rng.cloneRange();
|
|
5347
|
+
fromStart.setStart(a, 0);
|
|
5348
|
+
if (isBlank(toEnd.toString())) {
|
|
5348
5349
|
native.selection.setCursorLocation(a.parentNode, native.dom.nodeIndex(a) + 1);
|
|
5349
|
-
}
|
|
5350
|
-
|
|
5350
|
+
} else if (isBlank(fromStart.toString())) {
|
|
5351
|
+
native.selection.setCursorLocation(a.parentNode, native.dom.nodeIndex(a));
|
|
5351
5352
|
}
|
|
5352
|
-
}
|
|
5353
|
+
} catch (e) {
|
|
5354
|
+
console.warn("[editor] could not move the caret off the link edge:", e?.message || e);
|
|
5355
|
+
}
|
|
5356
|
+
};
|
|
5357
|
+
native.on("paste", () => {
|
|
5358
|
+
setTimeout(stepOutOfLinkEdge, 0);
|
|
5359
|
+
});
|
|
5360
|
+
native.on("keydown", (e) => {
|
|
5361
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key.length !== 1)
|
|
5362
|
+
return;
|
|
5363
|
+
stepOutOfLinkEdge();
|
|
5353
5364
|
});
|
|
5354
5365
|
}
|
|
5355
5366
|
|
|
@@ -6248,7 +6259,7 @@ function applyInit(init) {
|
|
|
6248
6259
|
sigHtml = acct.signature;
|
|
6249
6260
|
}
|
|
6250
6261
|
if (sigHtml) {
|
|
6251
|
-
const sigRule = '<hr style="width:3cm;margin:0.8em 0 0.5em 0;border:0;border-top:1px solid #999;">';
|
|
6262
|
+
const sigRule = '<hr class="mailx-sig-rule" style="width:3cm;margin:0.8em 0 0.5em 0;border:0;border-top:1px solid #999;">';
|
|
6252
6263
|
const sigBlock = `<br>${sigRule}${sigHtml}`;
|
|
6253
6264
|
bodyToRender = isReplyForward ? `<br>${sigBlock}<br>${bodyToRender}` : `${bodyToRender}${sigBlock}`;
|
|
6254
6265
|
}
|
|
@@ -7039,6 +7050,18 @@ var extEditHintClose = null;
|
|
|
7039
7050
|
}
|
|
7040
7051
|
}
|
|
7041
7052
|
var wordEditOpening = false;
|
|
7053
|
+
function splitWordEditable(html) {
|
|
7054
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
7055
|
+
const nodes = Array.from(doc.body.childNodes);
|
|
7056
|
+
const cutAt = nodes.findIndex((n) => n instanceof HTMLElement && n.matches('hr.mailx-sig-rule, hr[style*="3cm"], div.reply'));
|
|
7057
|
+
if (cutAt < 0) return { head: html, tail: "" };
|
|
7058
|
+
const serialize = (part) => {
|
|
7059
|
+
const box = doc.createElement("div");
|
|
7060
|
+
for (const n of part) box.appendChild(n.cloneNode(true));
|
|
7061
|
+
return box.innerHTML;
|
|
7062
|
+
};
|
|
7063
|
+
return { head: serialize(nodes.slice(0, cutAt)), tail: serialize(nodes.slice(cutAt)) };
|
|
7064
|
+
}
|
|
7042
7065
|
document.getElementById("btn-edit-in-word")?.addEventListener("click", async () => {
|
|
7043
7066
|
const btn = document.getElementById("btn-edit-in-word");
|
|
7044
7067
|
if (wordEditOpening) return;
|
|
@@ -7050,7 +7073,9 @@ document.getElementById("btn-edit-in-word")?.addEventListener("click", async ()
|
|
|
7050
7073
|
if (!wordEditId) wordEditId = `compose-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
7051
7074
|
showDraftStatus("Opening in Word\u2026 (this takes a few seconds)", false);
|
|
7052
7075
|
try {
|
|
7053
|
-
const
|
|
7076
|
+
const { head, tail } = splitWordEditable(editor.getHtml());
|
|
7077
|
+
logClientEvent("word-edit-split", { headChars: head.length, tailChars: tail.length, editId: wordEditId });
|
|
7078
|
+
const result = await openInWord(wordEditId, head, popoutInitId || "");
|
|
7054
7079
|
if (!result.ok || result.opener === "none") {
|
|
7055
7080
|
showDraftStatus("Couldn't launch an editor. Install Word, LibreOffice, or set a default for .html.", true);
|
|
7056
7081
|
return;
|
|
@@ -7210,8 +7235,9 @@ onEvent((ev) => {
|
|
|
7210
7235
|
}
|
|
7211
7236
|
try {
|
|
7212
7237
|
extEditHintClose?.();
|
|
7213
|
-
|
|
7214
|
-
|
|
7238
|
+
const { tail } = splitWordEditable(editor.getHtml());
|
|
7239
|
+
logClientEvent("word-edit-applied", { chars: (ev.html || "").length, tailChars: tail.length, editId: ev.editId });
|
|
7240
|
+
editor.setHtml((ev.html || "") + tail);
|
|
7215
7241
|
showDraftStatus("Reloaded edits from external editor.", false);
|
|
7216
7242
|
scheduleDraftSave();
|
|
7217
7243
|
} catch (e) {
|