From aae70d442d0d94f3ca6b995318770400f3f3fa1e Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:33:06 -0400 Subject: [PATCH 1/3] Improve blog embed resize coordination --- js/blog/index.js | 62 +++++++++++++++++++++++++++++++++++++++--------- views/blog.html | 62 +++++++++++++++++++++++------------------------- 2 files changed, 81 insertions(+), 43 deletions(-) diff --git a/js/blog/index.js b/js/blog/index.js index 73cd5741..5618223b 100644 --- a/js/blog/index.js +++ b/js/blog/index.js @@ -133,7 +133,12 @@ function resolveNotes() { } function setupEmbedHeightBroadcast({ root, registerCleanup }) { - if (typeof window === 'undefined' || !root || window.parent === window) { + if (typeof window === 'undefined' || window.parent === window) { + return; + } + + const element = root || document.querySelector('.blog-app-root'); + if (!element) { return; } @@ -149,11 +154,12 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { })(); let lastHeight = null; + let pendingFrame = null; - const postHeight = (force = false) => { - const rect = root.getBoundingClientRect(); - const height = Math.ceil(rect.height); - if (!Number.isFinite(height) || height < 0) { + const measureAndPost = (force = false) => { + const rect = element.getBoundingClientRect(); + const height = Math.max(0, Math.ceil(rect.height)); + if (!Number.isFinite(height)) { return; } if (!force && height === lastHeight) { @@ -167,21 +173,47 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { } }; - const rafId = window.requestAnimationFrame(() => { - postHeight(true); + const queueMeasurement = (force = false) => { + if (force) { + measureAndPost(true); + return; + } + if (pendingFrame != null) { + return; + } + pendingFrame = window.requestAnimationFrame(() => { + pendingFrame = null; + measureAndPost(false); + }); + }; + + pendingFrame = window.requestAnimationFrame(() => { + pendingFrame = null; + measureAndPost(true); }); + registerCleanup(() => { - window.cancelAnimationFrame(rafId); + if (pendingFrame != null) { + window.cancelAnimationFrame(pendingFrame); + pendingFrame = null; + } }); if (typeof ResizeObserver === 'function') { const observer = new ResizeObserver(() => { - postHeight(); + queueMeasurement(); }); - observer.observe(root); + observer.observe(element); registerCleanup(() => { observer.disconnect(); }); + } else { + const intervalId = window.setInterval(() => { + queueMeasurement(); + }, 500); + registerCleanup(() => { + window.clearInterval(intervalId); + }); } const handleMessage = (event) => { @@ -192,7 +224,7 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { return; } if (event?.data?.type === 'bitvid-blog-request-height') { - postHeight(true); + queueMeasurement(true); } }; @@ -200,6 +232,14 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { registerCleanup(() => { window.removeEventListener('message', handleMessage); }); + + const handleLoad = () => { + queueMeasurement(true); + }; + window.addEventListener('load', handleLoad, { once: true }); + registerCleanup(() => { + window.removeEventListener('load', handleLoad); + }); } function formatDate(unixSeconds, { includeTime = false } = {}) { diff --git a/views/blog.html b/views/blog.html index 74f94a99..0d5448db 100644 --- a/views/blog.html +++ b/views/blog.html @@ -35,8 +35,29 @@ iframe.style.removeProperty('max-height'); }; - let stopRetryLoop = () => {}; let receivedFirstHeight = false; + let retryIntervalId = null; + + const stopRequestLoop = () => { + if (retryIntervalId === null) { + return; + } + window.clearInterval(retryIntervalId); + retryIntervalId = null; + }; + + const startRequestLoop = () => { + if (retryIntervalId !== null) { + return; + } + retryIntervalId = window.setInterval(() => { + if (receivedFirstHeight || signal.aborted) { + stopRequestLoop(); + return; + } + requestHeight(); + }, 250); + }; const handleMessage = (event) => { if (event.source !== iframe.contentWindow) { @@ -48,7 +69,7 @@ if (event.data?.type === 'bitvid-blog-resize') { applyHeight(event.data.height); receivedFirstHeight = true; - stopRetryLoop(); + stopRequestLoop(); } }; @@ -64,35 +85,6 @@ ); }; - const startRetryLoop = () => { - const maxAttempts = 5; - let attempts = 0; - const retryIntervalMs = 200; - const intervalId = window.setInterval(() => { - attempts += 1; - if (attempts > maxAttempts || receivedFirstHeight) { - window.clearInterval(intervalId); - return; - } - requestHeight(); - }, retryIntervalMs); - - const stop = () => { - window.clearInterval(intervalId); - }; - - if (signal.aborted) { - stop(); - } else { - signal.addEventListener('abort', stop, { once: true }); - } - - stopRetryLoop = () => { - stop(); - stopRetryLoop = () => {}; - }; - }; - let hasStarted = false; const runInitialRequests = () => { @@ -101,7 +93,12 @@ } hasStarted = true; requestHeight(); - startRetryLoop(); + startRequestLoop(); + if (signal.aborted) { + stopRequestLoop(); + } else { + signal.addEventListener('abort', stopRequestLoop, { once: true }); + } }; iframe.addEventListener('load', () => { @@ -119,6 +116,7 @@ window.addEventListener('pagehide', () => { abortController.abort(); + stopRequestLoop(); }, { once: true }); } From a9dbf18b30147d192327ba5f3bef8a1f471a88cd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:40:05 -0400 Subject: [PATCH 2/3] Improve blog embed height measurement --- js/blog/index.js | 92 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/js/blog/index.js b/js/blog/index.js index 5618223b..56016962 100644 --- a/js/blog/index.js +++ b/js/blog/index.js @@ -157,8 +157,45 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { let pendingFrame = null; const measureAndPost = (force = false) => { - const rect = element.getBoundingClientRect(); - const height = Math.max(0, Math.ceil(rect.height)); + const docElement = document.documentElement; + const body = document.body; + + const candidates = []; + const addCandidate = (value) => { + if (typeof value !== 'number') { + return; + } + const rounded = Math.max(0, Math.ceil(value)); + if (Number.isFinite(rounded)) { + candidates.push(rounded); + } + }; + + const addElementMetrics = (node) => { + if (!node) { + return; + } + addCandidate(node.scrollHeight); + addCandidate(node.offsetHeight); + addCandidate(node.clientHeight); + }; + + try { + const rect = element.getBoundingClientRect(); + addCandidate(rect.height); + } catch (error) { + userLogger.error('[blog] failed to compute embed rect for resize messaging', error); + } + + addElementMetrics(element); + if (body && body !== element) { + addElementMetrics(body); + } + if (docElement && docElement !== element && docElement !== body) { + addElementMetrics(docElement); + } + + const height = candidates.length > 0 ? Math.max(...candidates) : null; if (!Number.isFinite(height)) { return; } @@ -199,13 +236,30 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { } }); + const observedNodes = new Set(); + + const observeNode = (observer, node) => { + if (!node || observedNodes.has(node)) { + return; + } + observer.observe(node); + observedNodes.add(node); + }; + if (typeof ResizeObserver === 'function') { const observer = new ResizeObserver(() => { queueMeasurement(); }); - observer.observe(element); + observeNode(observer, element); + if (document.body) { + observeNode(observer, document.body); + } + if (document.documentElement) { + observeNode(observer, document.documentElement); + } registerCleanup(() => { observer.disconnect(); + observedNodes.clear(); }); } else { const intervalId = window.setInterval(() => { @@ -216,6 +270,30 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { }); } + const mutationTargets = [element]; + if (document.body && document.body !== element) { + mutationTargets.push(document.body); + } + if (document.documentElement && document.documentElement !== element && document.documentElement !== document.body) { + mutationTargets.push(document.documentElement); + } + + if (typeof MutationObserver === 'function' && mutationTargets.length > 0) { + const mutationObserver = new MutationObserver(() => { + queueMeasurement(); + }); + mutationTargets.forEach((target) => { + mutationObserver.observe(target, { + childList: true, + subtree: true, + attributes: false, + }); + }); + registerCleanup(() => { + mutationObserver.disconnect(); + }); + } + const handleMessage = (event) => { if (event.source !== window.parent) { return; @@ -233,6 +311,14 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { window.removeEventListener('message', handleMessage); }); + const handleResize = () => { + queueMeasurement(); + }; + window.addEventListener('resize', handleResize); + registerCleanup(() => { + window.removeEventListener('resize', handleResize); + }); + const handleLoad = () => { queueMeasurement(true); }; From 9ddf3c8e2d2b251b24a1e8c99f9a9ccc91bea5ef Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:00:43 -0400 Subject: [PATCH 3/3] Improve blog embed resize robustness --- js/blog/index.js | 206 ++++++++++++++++++++++++++++++++++++++++++----- views/blog.html | 3 + 2 files changed, 189 insertions(+), 20 deletions(-) diff --git a/js/blog/index.js b/js/blog/index.js index 56016962..725c7641 100644 --- a/js/blog/index.js +++ b/js/blog/index.js @@ -153,10 +153,48 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { return '*'; })(); + let destroyed = false; + registerCleanup(() => { + destroyed = true; + }); + + const parseCssNumber = (value) => { + if (typeof value !== 'string') { + return 0; + } + const numeric = Number.parseFloat(value); + return Number.isFinite(numeric) ? numeric : 0; + }; + + const computeMarginAdjustment = (node) => { + if (!node || typeof window.getComputedStyle !== 'function') { + return 0; + } + let topMargin = 0; + let bottomMargin = 0; + try { + const first = node.firstElementChild; + const last = node.lastElementChild; + if (first) { + topMargin = parseCssNumber(window.getComputedStyle(first).marginTop); + } + if (last) { + bottomMargin = parseCssNumber(window.getComputedStyle(last).marginBottom); + } + } catch (error) { + userLogger.error('[blog] failed to compute margin adjustment for resize messaging', error); + } + const adjustment = Math.max(0, topMargin) + Math.max(0, bottomMargin); + return Number.isFinite(adjustment) ? adjustment : 0; + }; + let lastHeight = null; let pendingFrame = null; const measureAndPost = (force = false) => { + if (destroyed) { + return; + } const docElement = document.documentElement; const body = document.body; @@ -175,18 +213,18 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { if (!node) { return; } - addCandidate(node.scrollHeight); - addCandidate(node.offsetHeight); - addCandidate(node.clientHeight); + const marginAdjustment = computeMarginAdjustment(node); + addCandidate(node.scrollHeight + marginAdjustment); + addCandidate(node.offsetHeight + marginAdjustment); + addCandidate(node.clientHeight + marginAdjustment); + try { + const rect = node.getBoundingClientRect(); + addCandidate(rect.height + marginAdjustment); + } catch (error) { + userLogger.error('[blog] failed to compute element rect for resize messaging', error); + } }; - try { - const rect = element.getBoundingClientRect(); - addCandidate(rect.height); - } catch (error) { - userLogger.error('[blog] failed to compute embed rect for resize messaging', error); - } - addElementMetrics(element); if (body && body !== element) { addElementMetrics(body); @@ -194,6 +232,9 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { if (docElement && docElement !== element && docElement !== body) { addElementMetrics(docElement); } + if (document.scrollingElement && document.scrollingElement !== body && document.scrollingElement !== docElement) { + addElementMetrics(document.scrollingElement); + } const height = candidates.length > 0 ? Math.max(...candidates) : null; if (!Number.isFinite(height)) { @@ -211,6 +252,9 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { }; const queueMeasurement = (force = false) => { + if (destroyed) { + return; + } if (force) { measureAndPost(true); return; @@ -257,19 +301,22 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { if (document.documentElement) { observeNode(observer, document.documentElement); } + if (document.scrollingElement) { + observeNode(observer, document.scrollingElement); + } registerCleanup(() => { observer.disconnect(); observedNodes.clear(); }); - } else { - const intervalId = window.setInterval(() => { - queueMeasurement(); - }, 500); - registerCleanup(() => { - window.clearInterval(intervalId); - }); } + const pollingIntervalId = window.setInterval(() => { + queueMeasurement(); + }, 750); + registerCleanup(() => { + window.clearInterval(pollingIntervalId); + }); + const mutationTargets = [element]; if (document.body && document.body !== element) { mutationTargets.push(document.body); @@ -277,16 +324,85 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { if (document.documentElement && document.documentElement !== element && document.documentElement !== document.body) { mutationTargets.push(document.documentElement); } + if ( + document.scrollingElement && + document.scrollingElement !== element && + document.scrollingElement !== document.body && + document.scrollingElement !== document.documentElement + ) { + mutationTargets.push(document.scrollingElement); + } + + const trackedMediaNodes = new WeakSet(); + const MEDIA_SELECTOR = 'img, picture, video, audio, iframe, object, embed'; + + const watchMediaNode = (node) => { + if (!(node instanceof Element)) { + return; + } + const candidates = []; + if (node.matches(MEDIA_SELECTOR)) { + candidates.push(node); + } + candidates.push(...node.querySelectorAll(MEDIA_SELECTOR)); + candidates.forEach((media) => { + if (trackedMediaNodes.has(media)) { + return; + } + trackedMediaNodes.add(media); + const handleMediaEvent = () => { + queueMeasurement(true); + }; + const mediaEvents = ['load', 'loadeddata', 'loadedmetadata', 'readystatechange', 'error']; + mediaEvents.forEach((eventName) => { + media.addEventListener(eventName, handleMediaEvent); + }); + registerCleanup(() => { + mediaEvents.forEach((eventName) => { + media.removeEventListener(eventName, handleMediaEvent); + }); + }); + if ( + (media instanceof HTMLImageElement && media.complete) || + (media instanceof HTMLVideoElement && media.readyState >= 1) || + (media instanceof HTMLAudioElement && media.readyState >= 1) || + (media instanceof HTMLIFrameElement && media.contentWindow) + ) { + queueMeasurement(true); + } + }); + }; + + watchMediaNode(element); if (typeof MutationObserver === 'function' && mutationTargets.length > 0) { - const mutationObserver = new MutationObserver(() => { - queueMeasurement(); + const mutationObserver = new MutationObserver((mutations) => { + let shouldMeasure = false; + mutations.forEach((mutation) => { + for (const node of mutation.addedNodes) { + watchMediaNode(node); + } + if (mutation.type === 'attributes') { + watchMediaNode(mutation.target); + } + if ( + mutation.type === 'childList' || + mutation.type === 'attributes' || + mutation.type === 'characterData' + ) { + shouldMeasure = true; + } + }); + if (shouldMeasure) { + queueMeasurement(); + } }); mutationTargets.forEach((target) => { mutationObserver.observe(target, { childList: true, subtree: true, - attributes: false, + attributes: true, + characterData: true, }); }); registerCleanup(() => { @@ -319,6 +435,24 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { window.removeEventListener('resize', handleResize); }); + const handleOrientationChange = () => { + queueMeasurement(true); + }; + window.addEventListener('orientationchange', handleOrientationChange); + registerCleanup(() => { + window.removeEventListener('orientationchange', handleOrientationChange); + }); + + const handleTransitionEnd = () => { + queueMeasurement(); + }; + element.addEventListener('transitionend', handleTransitionEnd, true); + element.addEventListener('animationend', handleTransitionEnd, true); + registerCleanup(() => { + element.removeEventListener('transitionend', handleTransitionEnd, true); + element.removeEventListener('animationend', handleTransitionEnd, true); + }); + const handleLoad = () => { queueMeasurement(true); }; @@ -326,6 +460,38 @@ function setupEmbedHeightBroadcast({ root, registerCleanup }) { registerCleanup(() => { window.removeEventListener('load', handleLoad); }); + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + queueMeasurement(true); + } + }; + document.addEventListener('visibilitychange', handleVisibilityChange); + registerCleanup(() => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }); + + if (document.fonts && typeof document.fonts.addEventListener === 'function') { + const handleFontLoad = () => { + queueMeasurement(true); + }; + document.fonts.addEventListener('loadingdone', handleFontLoad); + document.fonts.addEventListener('loadingerror', handleFontLoad); + registerCleanup(() => { + document.fonts.removeEventListener('loadingdone', handleFontLoad); + document.fonts.removeEventListener('loadingerror', handleFontLoad); + }); + } + + if (document.fonts?.ready && typeof document.fonts.ready.then === 'function') { + document.fonts.ready + .then(() => { + queueMeasurement(true); + }) + .catch(() => { + queueMeasurement(true); + }); + } } function formatDate(unixSeconds, { includeTime = false } = {}) { diff --git a/views/blog.html b/views/blog.html index 0d5448db..50af79a6 100644 --- a/views/blog.html +++ b/views/blog.html @@ -102,6 +102,9 @@ }; iframe.addEventListener('load', () => { + receivedFirstHeight = false; + stopRequestLoop(); + hasStarted = false; runInitialRequests(); }, { signal });