Fix: render email body on DOM parse, not iframe load #635
This commit is contained in:
@@ -2190,8 +2190,20 @@ export function EmailViewer({
|
|||||||
// Gates the quick reply on the iframe having loaded the current srcDoc, so
|
// Gates the quick reply on the iframe having loaded the current srcDoc, so
|
||||||
// it doesn't flash in below a still-resizing iframe.
|
// it doesn't flash in below a still-resizing iframe.
|
||||||
const [iframeReady, setIframeReady] = useState(false);
|
const [iframeReady, setIframeReady] = useState(false);
|
||||||
|
// Tracks which parsed document we've already wired up, so setup runs exactly
|
||||||
|
// once per srcDoc even though both the readiness poll below and the iframe
|
||||||
|
// 'load' event can trigger it.
|
||||||
|
const initializedDocRef = useRef<Document | null>(null);
|
||||||
|
// The document present at the instant srcDoc changed - i.e. the one about to
|
||||||
|
// be torn down. contentDocument keeps pointing at it until the browser swaps
|
||||||
|
// the new srcDoc in, so the poll skips it to avoid wiring up stale content.
|
||||||
|
const staleDocRef = useRef<Document | null>(null);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setIframeReady(false);
|
setIframeReady(false);
|
||||||
|
initializedDocRef.current = null;
|
||||||
|
// Runs during commit, before the browser processes the new srcDoc, so
|
||||||
|
// contentDocument here is still the outgoing document.
|
||||||
|
staleDocRef.current = iframeRef.current?.contentDocument ?? null;
|
||||||
}, [emailIframeSrcDoc]);
|
}, [emailIframeSrcDoc]);
|
||||||
|
|
||||||
const handleIframeLoad = useCallback(() => {
|
const handleIframeLoad = useCallback(() => {
|
||||||
@@ -2199,7 +2211,13 @@ export function EmailViewer({
|
|||||||
if (!iframe) return;
|
if (!iframe) return;
|
||||||
try {
|
try {
|
||||||
const doc = iframe.contentDocument;
|
const doc = iframe.contentDocument;
|
||||||
if (doc?.body) {
|
// Ignore the outgoing document, a transient about:blank (a fresh srcDoc
|
||||||
|
// document reports URL 'about:srcdoc'), and anything that hasn't finished
|
||||||
|
// parsing yet; run the setup below at most once per document.
|
||||||
|
if (!doc?.body || doc === staleDocRef.current || doc.URL !== 'about:srcdoc' || doc.readyState === 'loading') return;
|
||||||
|
if (initializedDocRef.current === doc) return;
|
||||||
|
initializedDocRef.current = doc;
|
||||||
|
{
|
||||||
// Auto-resize iframe to fit content
|
// Auto-resize iframe to fit content
|
||||||
// Measure max(documentElement, body): a height:100% wrapper can leave
|
// Measure max(documentElement, body): a height:100% wrapper can leave
|
||||||
// documentElement.scrollHeight short while the real content lives in body.
|
// documentElement.scrollHeight short while the real content lives in body.
|
||||||
@@ -2375,6 +2393,26 @@ export function EmailViewer({
|
|||||||
}
|
}
|
||||||
}, [isDark, emailHasNativeDarkMode, email?.id]);
|
}, [isDark, emailHasNativeDarkMode, email?.id]);
|
||||||
|
|
||||||
|
// Wire up the iframe as soon as its sandboxed document has parsed, rather than
|
||||||
|
// waiting for the iframe 'load' event. 'load' also waits on every subresource,
|
||||||
|
// so a single unreachable remote image (server accepts the TCP connection but
|
||||||
|
// never responds) stalls it for the browser's ~60s timeout - freezing the body
|
||||||
|
// at its placeholder height that entire time. The parsed DOM we need for
|
||||||
|
// height, links and dark-mode is ready long before images resolve. Poll the
|
||||||
|
// fresh document's readyState because a sandbox without allow-scripts can't
|
||||||
|
// postMessage a DOMContentLoaded signal out, and the onLoad handler is
|
||||||
|
// idempotent per document so it stays a harmless backstop.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!iframeRef.current) return;
|
||||||
|
const readyPoll = window.setInterval(() => {
|
||||||
|
handleIframeLoad();
|
||||||
|
if (initializedDocRef.current) window.clearInterval(readyPoll);
|
||||||
|
}, 50);
|
||||||
|
// Safety stop: the 'load' backstop covers anything the poll somehow misses.
|
||||||
|
const stop = window.setTimeout(() => window.clearInterval(readyPoll), 15000);
|
||||||
|
return () => { window.clearInterval(readyPoll); window.clearTimeout(stop); };
|
||||||
|
}, [emailIframeSrcDoc, handleIframeLoad]);
|
||||||
|
|
||||||
// Export email as .eml file
|
// Export email as .eml file
|
||||||
const handleExportEmail = async () => {
|
const handleExportEmail = async () => {
|
||||||
if (!email?.blobId || !client) return;
|
if (!email?.blobId || !client) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user