@bobfrankston/rmfmail 1.2.238 → 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/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-55168 → node_modules.npmglobalize-stash-57024}/.package-lock.json +0 -0
|
@@ -1559,43 +1559,22 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
1559
1559
|
installPreviewControls(iframe);
|
|
1560
1560
|
}
|
|
1561
1561
|
else if (msg.bodyText) {
|
|
1562
|
-
|
|
1563
|
-
//
|
|
1564
|
-
// font, same size + line-height. Default `<pre>` rendering is
|
|
1565
|
-
// monospace, which on some platforms substitutes to a serif
|
|
1566
|
-
// fallback (Bob 2026-05-12: "why the font change") and looks
|
|
1567
|
-
// like a different message from the same conversation.
|
|
1568
|
-
// height:100% + overflow:auto make the <pre> its own scroll
|
|
1569
|
-
// container. Without them the <pre> grows to its full content
|
|
1570
|
-
// height and the parent .mv-body (overflow:hidden) silently
|
|
1571
|
-
// clips everything below the fold — no scrollbar, wheel dead
|
|
1572
|
-
// (Bob 2026-05-22). The HTML-body branch gets this for free via
|
|
1573
|
-
// the iframe; plain-text bodies need it explicitly.
|
|
1574
|
-
pre.style.cssText = "padding: 1rem; white-space: pre-wrap; word-break: break-word; "
|
|
1575
|
-
+ "font-family: system-ui, sans-serif; font-size: 17.5px; line-height: 1.5; "
|
|
1576
|
-
+ "color: #1a1a2e; background: #fff; margin: 0; height: 100%; overflow: auto;";
|
|
1577
|
-
// Auto-linkify URLs in plain text.
|
|
1562
|
+
// Plain text renders through the SAME sandboxed iframe as an HTML
|
|
1563
|
+
// body, as a <pre> of linkified text.
|
|
1578
1564
|
//
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
//
|
|
1565
|
+
// It used to be a <pre> in the PARENT document, and that is why
|
|
1566
|
+
// "hovering over a URL or right mousing isn't working" in a
|
|
1567
|
+
// plain-text message (Bob 2026-08-09): every link affordance the
|
|
1568
|
+
// preview has — hover target, the right-click link menu (Open /
|
|
1569
|
+
// Copy link address / search), tap-to-open-externally — is
|
|
1570
|
+
// implemented for the iframe, by its injected script and its
|
|
1571
|
+
// contextmenu → previewContextMenu bridge. A detected URL isn't
|
|
1572
|
+
// "less of a URL" than an <a> the sender wrote, so it goes through
|
|
1573
|
+
// the same path and behaves identically, instead of the parent
|
|
1574
|
+
// document's generic fallback menu.
|
|
1586
1575
|
//
|
|
1587
|
-
//
|
|
1588
|
-
//
|
|
1589
|
-
// paint the first screenful immediately, then append the rest in
|
|
1590
|
-
// line-aligned chunks across animation frames. Same content, same
|
|
1591
|
-
// links, but the main thread comes up for air between chunks so
|
|
1592
|
-
// the app stays live and the text streams in visibly.
|
|
1593
|
-
if (msg.bodyText.length > PROGRESSIVE_TEXT_THRESHOLD) {
|
|
1594
|
-
renderTextProgressively(pre, msg.bodyText, gen);
|
|
1595
|
-
}
|
|
1596
|
-
else {
|
|
1597
|
-
pre.innerHTML = linkifyText(msg.bodyText);
|
|
1598
|
-
}
|
|
1576
|
+
// Same reason as every other unification today: two renderers for
|
|
1577
|
+
// one job means every fix lands in one of them.
|
|
1599
1578
|
// Oversized body: the service sent only the head, because the IPC
|
|
1600
1579
|
// channel marshals replies through evaluate_script and a multi-MB
|
|
1601
1580
|
// body takes the WebView2 renderer down with it. Say so — showing
|
|
@@ -1614,7 +1593,29 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
1614
1593
|
<code style="user-select:all;font-size:0.9em">${escapeHtml(trunc.emlPath || "(path unavailable)")}</code></div>`;
|
|
1615
1594
|
bodyEl.appendChild(note);
|
|
1616
1595
|
}
|
|
1617
|
-
|
|
1596
|
+
const iframe = document.createElement("iframe");
|
|
1597
|
+
iframe.sandbox.add("allow-same-origin");
|
|
1598
|
+
iframe.sandbox.add("allow-popups");
|
|
1599
|
+
iframe.sandbox.add("allow-popups-to-escape-sandbox");
|
|
1600
|
+
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
|
1601
|
+
iframe.sandbox.add("allow-scripts");
|
|
1602
|
+
// `rmf-plain` is styled by wrapHtmlBody so the typography matches
|
|
1603
|
+
// what the parent <pre> used to produce.
|
|
1604
|
+
const head = msg.bodyText.length > PROGRESSIVE_TEXT_THRESHOLD
|
|
1605
|
+
? msg.bodyText.slice(0, firstChunkEnd(msg.bodyText))
|
|
1606
|
+
: msg.bodyText;
|
|
1607
|
+
iframe.srcdoc = wrapHtmlBody(`<pre class="rmf-plain" id="rmf-plain">${linkifyText(head)}</pre>`, false);
|
|
1608
|
+
iframe.addEventListener("load", () => _ptick("iframe load (all resources)"), { once: true });
|
|
1609
|
+
bodyEl.appendChild(iframe);
|
|
1610
|
+
_ptick("iframe srcdoc set (plain text)");
|
|
1611
|
+
installPreviewControls(iframe);
|
|
1612
|
+
// Big body: stream the remainder into the iframe across animation
|
|
1613
|
+
// frames rather than parsing megabytes in one blocking gulp
|
|
1614
|
+
// (v1.2.229's reason, preserved — the work just happens inside the
|
|
1615
|
+
// iframe document now).
|
|
1616
|
+
if (msg.bodyText.length > head.length) {
|
|
1617
|
+
appendTextProgressively(iframe, msg.bodyText.slice(head.length), gen);
|
|
1618
|
+
}
|
|
1618
1619
|
}
|
|
1619
1620
|
else {
|
|
1620
1621
|
// No bodyHtml AND no bodyText. The daemon thinks the body is
|
|
@@ -1968,47 +1969,73 @@ const PROGRESSIVE_TEXT_THRESHOLD = 200 * 1024;
|
|
|
1968
1969
|
* 16 ms budget on a slow machine; large enough that a 1.4 MB body finishes in
|
|
1969
1970
|
* a couple of dozen frames rather than hundreds. */
|
|
1970
1971
|
const TEXT_CHUNK_BYTES = 64 * 1024;
|
|
1972
|
+
/** Where the first chunk of a large plain-text body ends — cut on a newline
|
|
1973
|
+
* so a URL is never split across chunks (half a URL linkifies wrong). */
|
|
1974
|
+
function firstChunkEnd(text) {
|
|
1975
|
+
const end = Math.min(TEXT_CHUNK_BYTES, text.length);
|
|
1976
|
+
if (end >= text.length)
|
|
1977
|
+
return text.length;
|
|
1978
|
+
const nl = text.lastIndexOf("\n", end);
|
|
1979
|
+
return nl > 0 ? nl + 1 : end;
|
|
1980
|
+
}
|
|
1971
1981
|
/**
|
|
1972
|
-
*
|
|
1973
|
-
* frame, linkifying each chunk as it goes.
|
|
1982
|
+
* Stream the rest of a large plain-text body into the preview iframe, one
|
|
1983
|
+
* line-aligned chunk per animation frame, linkifying each chunk as it goes.
|
|
1974
1984
|
*
|
|
1975
|
-
*
|
|
1976
|
-
*
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
1985
|
+
* Why chunked at all: linkify + HTML parse + layout over a multi-megabyte
|
|
1986
|
+
* body is synchronous, so doing it in one statement freezes the window long
|
|
1987
|
+
* enough for WebView2 to report the renderer unresponsive (Bob 2026-08-08,
|
|
1988
|
+
* "oh — just took forever"). A Web Worker cannot help: workers have no DOM,
|
|
1989
|
+
* and the DOM work IS the expensive half. Coming up for air between chunks is
|
|
1990
|
+
* what keeps the app live, and the text visibly streams in.
|
|
1979
1991
|
*
|
|
1980
|
-
* Aborts if the user moves to another message mid-render
|
|
1981
|
-
*
|
|
1992
|
+
* Aborts if the user moves to another message mid-render (`gen` against
|
|
1993
|
+
* showMessageGeneration, the same staleness guard the IPC path uses) or if
|
|
1994
|
+
* the iframe is torn out from under us.
|
|
1982
1995
|
*/
|
|
1983
|
-
function
|
|
1996
|
+
function appendTextProgressively(iframe, rest, gen) {
|
|
1984
1997
|
const chunks = [];
|
|
1985
1998
|
let at = 0;
|
|
1986
|
-
while (at <
|
|
1987
|
-
let end = Math.min(at + TEXT_CHUNK_BYTES,
|
|
1988
|
-
if (end <
|
|
1989
|
-
const nl =
|
|
1999
|
+
while (at < rest.length) {
|
|
2000
|
+
let end = Math.min(at + TEXT_CHUNK_BYTES, rest.length);
|
|
2001
|
+
if (end < rest.length) {
|
|
2002
|
+
const nl = rest.lastIndexOf("\n", end);
|
|
1990
2003
|
if (nl > at)
|
|
1991
2004
|
end = nl + 1;
|
|
1992
2005
|
}
|
|
1993
|
-
chunks.push(
|
|
2006
|
+
chunks.push(rest.slice(at, end));
|
|
1994
2007
|
at = end;
|
|
1995
2008
|
}
|
|
1996
2009
|
let i = 0;
|
|
1997
2010
|
const step = () => {
|
|
1998
|
-
// Stale — another message is on screen now; stop appending to a <pre>
|
|
1999
|
-
// nobody is looking at.
|
|
2000
2011
|
if (gen !== showMessageGeneration)
|
|
2001
|
-
return;
|
|
2012
|
+
return; // another message is on screen
|
|
2013
|
+
const doc = iframe.contentDocument;
|
|
2014
|
+
const host = doc?.getElementById("rmf-plain");
|
|
2015
|
+
if (!doc || !host)
|
|
2016
|
+
return; // iframe replaced or not ready
|
|
2002
2017
|
if (i >= chunks.length)
|
|
2003
2018
|
return;
|
|
2004
|
-
const span =
|
|
2019
|
+
const span = doc.createElement("span");
|
|
2005
2020
|
span.innerHTML = linkifyText(chunks[i++]);
|
|
2006
|
-
|
|
2021
|
+
host.appendChild(span);
|
|
2007
2022
|
requestAnimationFrame(step);
|
|
2008
2023
|
};
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2011
|
-
|
|
2024
|
+
// The iframe document may not exist for a frame or two after srcdoc is
|
|
2025
|
+
// set; poll briefly rather than dropping the tail on the floor.
|
|
2026
|
+
let waits = 0;
|
|
2027
|
+
const begin = () => {
|
|
2028
|
+
if (gen !== showMessageGeneration)
|
|
2029
|
+
return;
|
|
2030
|
+
if (iframe.contentDocument?.getElementById("rmf-plain")) {
|
|
2031
|
+
step();
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
if (waits++ > 120)
|
|
2035
|
+
return; // ~2 s at 60 fps — give up quietly
|
|
2036
|
+
requestAnimationFrame(begin);
|
|
2037
|
+
};
|
|
2038
|
+
requestAnimationFrame(begin);
|
|
2012
2039
|
}
|
|
2013
2040
|
function linkifyText(text) {
|
|
2014
2041
|
// Escape HTML first
|
|
@@ -2180,6 +2207,19 @@ ${csp}
|
|
|
2180
2207
|
img { max-width: 100%; }
|
|
2181
2208
|
a { color: #1a6dd4; }
|
|
2182
2209
|
pre, code { white-space: pre-wrap; }
|
|
2210
|
+
/* Plain-text bodies render as this <pre>. Sans-serif at the same size and
|
|
2211
|
+
line-height as an HTML body, so two messages in one thread don't look
|
|
2212
|
+
like they came from different apps (Bob 2026-05-12: "why the font
|
|
2213
|
+
change"). */
|
|
2214
|
+
.rmf-plain {
|
|
2215
|
+
margin: 0;
|
|
2216
|
+
padding: 0;
|
|
2217
|
+
font-family: system-ui, sans-serif;
|
|
2218
|
+
font-size: 17.5px;
|
|
2219
|
+
line-height: 1.5;
|
|
2220
|
+
white-space: pre-wrap;
|
|
2221
|
+
word-break: break-word;
|
|
2222
|
+
}
|
|
2183
2223
|
blockquote { border-left: 3px solid #ccc; padding-left: 1rem; margin-left: 0; color: #666; }
|
|
2184
2224
|
@media (prefers-color-scheme: dark) {
|
|
2185
2225
|
body { color: #cdd6f4; background: #282840; }
|