@bobfrankston/rmfmail 1.2.237 → 1.2.239
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/TODO.md +6 -0
- package/client/app.bundle.js +60 -17
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +98 -58
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +93 -57
- package/client/compose/compose.bundle.js +2 -2
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +5 -2
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +5 -2
- package/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-40840 → node_modules.npmglobalize-stash-57024}/.package-lock.json +0 -0
|
@@ -1435,42 +1435,22 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
|
|
|
1435
1435
|
_ptick("iframe srcdoc set");
|
|
1436
1436
|
installPreviewControls(iframe);
|
|
1437
1437
|
} else if (msg.bodyText) {
|
|
1438
|
-
|
|
1439
|
-
//
|
|
1440
|
-
// font, same size + line-height. Default `<pre>` rendering is
|
|
1441
|
-
// monospace, which on some platforms substitutes to a serif
|
|
1442
|
-
// fallback (Bob 2026-05-12: "why the font change") and looks
|
|
1443
|
-
// like a different message from the same conversation.
|
|
1444
|
-
// height:100% + overflow:auto make the <pre> its own scroll
|
|
1445
|
-
// container. Without them the <pre> grows to its full content
|
|
1446
|
-
// height and the parent .mv-body (overflow:hidden) silently
|
|
1447
|
-
// clips everything below the fold — no scrollbar, wheel dead
|
|
1448
|
-
// (Bob 2026-05-22). The HTML-body branch gets this for free via
|
|
1449
|
-
// the iframe; plain-text bodies need it explicitly.
|
|
1450
|
-
pre.style.cssText = "padding: 1rem; white-space: pre-wrap; word-break: break-word; "
|
|
1451
|
-
+ "font-family: system-ui, sans-serif; font-size: 17.5px; line-height: 1.5; "
|
|
1452
|
-
+ "color: #1a1a2e; background: #fff; margin: 0; height: 100%; overflow: auto;";
|
|
1453
|
-
// Auto-linkify URLs in plain text.
|
|
1438
|
+
// Plain text renders through the SAME sandboxed iframe as an HTML
|
|
1439
|
+
// body, as a <pre> of linkified text.
|
|
1454
1440
|
//
|
|
1455
|
-
//
|
|
1456
|
-
//
|
|
1457
|
-
//
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1441
|
+
// It used to be a <pre> in the PARENT document, and that is why
|
|
1442
|
+
// "hovering over a URL or right mousing isn't working" in a
|
|
1443
|
+
// plain-text message (Bob 2026-08-09): every link affordance the
|
|
1444
|
+
// preview has — hover target, the right-click link menu (Open /
|
|
1445
|
+
// Copy link address / search), tap-to-open-externally — is
|
|
1446
|
+
// implemented for the iframe, by its injected script and its
|
|
1447
|
+
// contextmenu → previewContextMenu bridge. A detected URL isn't
|
|
1448
|
+
// "less of a URL" than an <a> the sender wrote, so it goes through
|
|
1449
|
+
// the same path and behaves identically, instead of the parent
|
|
1450
|
+
// document's generic fallback menu.
|
|
1462
1451
|
//
|
|
1463
|
-
//
|
|
1464
|
-
//
|
|
1465
|
-
// paint the first screenful immediately, then append the rest in
|
|
1466
|
-
// line-aligned chunks across animation frames. Same content, same
|
|
1467
|
-
// links, but the main thread comes up for air between chunks so
|
|
1468
|
-
// the app stays live and the text streams in visibly.
|
|
1469
|
-
if (msg.bodyText.length > PROGRESSIVE_TEXT_THRESHOLD) {
|
|
1470
|
-
renderTextProgressively(pre, msg.bodyText, gen);
|
|
1471
|
-
} else {
|
|
1472
|
-
pre.innerHTML = linkifyText(msg.bodyText);
|
|
1473
|
-
}
|
|
1452
|
+
// Same reason as every other unification today: two renderers for
|
|
1453
|
+
// one job means every fix lands in one of them.
|
|
1474
1454
|
// Oversized body: the service sent only the head, because the IPC
|
|
1475
1455
|
// channel marshals replies through evaluate_script and a multi-MB
|
|
1476
1456
|
// body takes the WebView2 renderer down with it. Say so — showing
|
|
@@ -1489,7 +1469,30 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
|
|
|
1489
1469
|
<code style="user-select:all;font-size:0.9em">${escapeHtml(trunc.emlPath || "(path unavailable)")}</code></div>`;
|
|
1490
1470
|
bodyEl.appendChild(note);
|
|
1491
1471
|
}
|
|
1492
|
-
|
|
1472
|
+
|
|
1473
|
+
const iframe = document.createElement("iframe");
|
|
1474
|
+
iframe.sandbox.add("allow-same-origin");
|
|
1475
|
+
iframe.sandbox.add("allow-popups");
|
|
1476
|
+
iframe.sandbox.add("allow-popups-to-escape-sandbox");
|
|
1477
|
+
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
|
1478
|
+
iframe.sandbox.add("allow-scripts");
|
|
1479
|
+
// `rmf-plain` is styled by wrapHtmlBody so the typography matches
|
|
1480
|
+
// what the parent <pre> used to produce.
|
|
1481
|
+
const head = msg.bodyText.length > PROGRESSIVE_TEXT_THRESHOLD
|
|
1482
|
+
? msg.bodyText.slice(0, firstChunkEnd(msg.bodyText))
|
|
1483
|
+
: msg.bodyText;
|
|
1484
|
+
iframe.srcdoc = wrapHtmlBody(`<pre class="rmf-plain" id="rmf-plain">${linkifyText(head)}</pre>`, false);
|
|
1485
|
+
iframe.addEventListener("load", () => _ptick("iframe load (all resources)"), { once: true });
|
|
1486
|
+
bodyEl.appendChild(iframe);
|
|
1487
|
+
_ptick("iframe srcdoc set (plain text)");
|
|
1488
|
+
installPreviewControls(iframe);
|
|
1489
|
+
// Big body: stream the remainder into the iframe across animation
|
|
1490
|
+
// frames rather than parsing megabytes in one blocking gulp
|
|
1491
|
+
// (v1.2.229's reason, preserved — the work just happens inside the
|
|
1492
|
+
// iframe document now).
|
|
1493
|
+
if (msg.bodyText.length > head.length) {
|
|
1494
|
+
appendTextProgressively(iframe, msg.bodyText.slice(head.length), gen);
|
|
1495
|
+
}
|
|
1493
1496
|
} else {
|
|
1494
1497
|
// No bodyHtml AND no bodyText. The daemon thinks the body is
|
|
1495
1498
|
// on disk (`cached === true`) but extracted nothing. Most
|
|
@@ -1814,44 +1817,64 @@ const PROGRESSIVE_TEXT_THRESHOLD = 200 * 1024;
|
|
|
1814
1817
|
* a couple of dozen frames rather than hundreds. */
|
|
1815
1818
|
const TEXT_CHUNK_BYTES = 64 * 1024;
|
|
1816
1819
|
|
|
1820
|
+
/** Where the first chunk of a large plain-text body ends — cut on a newline
|
|
1821
|
+
* so a URL is never split across chunks (half a URL linkifies wrong). */
|
|
1822
|
+
function firstChunkEnd(text: string): number {
|
|
1823
|
+
const end = Math.min(TEXT_CHUNK_BYTES, text.length);
|
|
1824
|
+
if (end >= text.length) return text.length;
|
|
1825
|
+
const nl = text.lastIndexOf("\n", end);
|
|
1826
|
+
return nl > 0 ? nl + 1 : end;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1817
1829
|
/**
|
|
1818
|
-
*
|
|
1819
|
-
* frame, linkifying each chunk as it goes.
|
|
1830
|
+
* Stream the rest of a large plain-text body into the preview iframe, one
|
|
1831
|
+
* line-aligned chunk per animation frame, linkifying each chunk as it goes.
|
|
1820
1832
|
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1833
|
+
* Why chunked at all: linkify + HTML parse + layout over a multi-megabyte
|
|
1834
|
+
* body is synchronous, so doing it in one statement freezes the window long
|
|
1835
|
+
* enough for WebView2 to report the renderer unresponsive (Bob 2026-08-08,
|
|
1836
|
+
* "oh — just took forever"). A Web Worker cannot help: workers have no DOM,
|
|
1837
|
+
* and the DOM work IS the expensive half. Coming up for air between chunks is
|
|
1838
|
+
* what keeps the app live, and the text visibly streams in.
|
|
1825
1839
|
*
|
|
1826
|
-
* Aborts if the user moves to another message mid-render
|
|
1827
|
-
*
|
|
1840
|
+
* Aborts if the user moves to another message mid-render (`gen` against
|
|
1841
|
+
* showMessageGeneration, the same staleness guard the IPC path uses) or if
|
|
1842
|
+
* the iframe is torn out from under us.
|
|
1828
1843
|
*/
|
|
1829
|
-
function
|
|
1844
|
+
function appendTextProgressively(iframe: HTMLIFrameElement, rest: string, gen: number): void {
|
|
1830
1845
|
const chunks: string[] = [];
|
|
1831
1846
|
let at = 0;
|
|
1832
|
-
while (at <
|
|
1833
|
-
let end = Math.min(at + TEXT_CHUNK_BYTES,
|
|
1834
|
-
if (end <
|
|
1835
|
-
const nl =
|
|
1847
|
+
while (at < rest.length) {
|
|
1848
|
+
let end = Math.min(at + TEXT_CHUNK_BYTES, rest.length);
|
|
1849
|
+
if (end < rest.length) {
|
|
1850
|
+
const nl = rest.lastIndexOf("\n", end);
|
|
1836
1851
|
if (nl > at) end = nl + 1;
|
|
1837
1852
|
}
|
|
1838
|
-
chunks.push(
|
|
1853
|
+
chunks.push(rest.slice(at, end));
|
|
1839
1854
|
at = end;
|
|
1840
1855
|
}
|
|
1841
1856
|
let i = 0;
|
|
1842
1857
|
const step = (): void => {
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1858
|
+
if (gen !== showMessageGeneration) return; // another message is on screen
|
|
1859
|
+
const doc = iframe.contentDocument;
|
|
1860
|
+
const host = doc?.getElementById("rmf-plain");
|
|
1861
|
+
if (!doc || !host) return; // iframe replaced or not ready
|
|
1846
1862
|
if (i >= chunks.length) return;
|
|
1847
|
-
const span =
|
|
1863
|
+
const span = doc.createElement("span");
|
|
1848
1864
|
span.innerHTML = linkifyText(chunks[i++]);
|
|
1849
|
-
|
|
1865
|
+
host.appendChild(span);
|
|
1850
1866
|
requestAnimationFrame(step);
|
|
1851
1867
|
};
|
|
1852
|
-
//
|
|
1853
|
-
//
|
|
1854
|
-
|
|
1868
|
+
// The iframe document may not exist for a frame or two after srcdoc is
|
|
1869
|
+
// set; poll briefly rather than dropping the tail on the floor.
|
|
1870
|
+
let waits = 0;
|
|
1871
|
+
const begin = (): void => {
|
|
1872
|
+
if (gen !== showMessageGeneration) return;
|
|
1873
|
+
if (iframe.contentDocument?.getElementById("rmf-plain")) { step(); return; }
|
|
1874
|
+
if (waits++ > 120) return; // ~2 s at 60 fps — give up quietly
|
|
1875
|
+
requestAnimationFrame(begin);
|
|
1876
|
+
};
|
|
1877
|
+
requestAnimationFrame(begin);
|
|
1855
1878
|
}
|
|
1856
1879
|
|
|
1857
1880
|
function linkifyText(text: string): string {
|
|
@@ -2020,6 +2043,19 @@ ${csp}
|
|
|
2020
2043
|
img { max-width: 100%; }
|
|
2021
2044
|
a { color: #1a6dd4; }
|
|
2022
2045
|
pre, code { white-space: pre-wrap; }
|
|
2046
|
+
/* Plain-text bodies render as this <pre>. Sans-serif at the same size and
|
|
2047
|
+
line-height as an HTML body, so two messages in one thread don't look
|
|
2048
|
+
like they came from different apps (Bob 2026-05-12: "why the font
|
|
2049
|
+
change"). */
|
|
2050
|
+
.rmf-plain {
|
|
2051
|
+
margin: 0;
|
|
2052
|
+
padding: 0;
|
|
2053
|
+
font-family: system-ui, sans-serif;
|
|
2054
|
+
font-size: 17.5px;
|
|
2055
|
+
line-height: 1.5;
|
|
2056
|
+
white-space: pre-wrap;
|
|
2057
|
+
word-break: break-word;
|
|
2058
|
+
}
|
|
2023
2059
|
blockquote { border-left: 3px solid #ccc; padding-left: 1rem; margin-left: 0; color: #666; }
|
|
2024
2060
|
@media (prefers-color-scheme: dark) {
|
|
2025
2061
|
body { color: #cdd6f4; background: #282840; }
|
|
@@ -6775,12 +6775,12 @@ function showExternalEditHint(editorLabel) {
|
|
|
6775
6775
|
onEvent((ev) => {
|
|
6776
6776
|
if (ev?.type !== "wordEditUpdated") return;
|
|
6777
6777
|
if (!wordEditId || ev.editId !== wordEditId) {
|
|
6778
|
-
|
|
6778
|
+
logClientEvent("word-edit-ignored", { forEditId: ev.editId, thisWindow: wordEditId || null });
|
|
6779
6779
|
return;
|
|
6780
6780
|
}
|
|
6781
6781
|
try {
|
|
6782
6782
|
extEditHintClose?.();
|
|
6783
|
-
|
|
6783
|
+
logClientEvent("word-edit-applied", { chars: (ev.html || "").length, editId: ev.editId });
|
|
6784
6784
|
editor.setHtml(ev.html || "");
|
|
6785
6785
|
showDraftStatus("Reloaded edits from external editor.", false);
|
|
6786
6786
|
scheduleDraftSave();
|