lazyloading
This commit is contained in:
@@ -23,6 +23,22 @@
|
||||
let chatFocused = document.hasFocus();
|
||||
const ytOembedCache = new Map(); // videoId → {title, author_name}
|
||||
|
||||
// Shared IntersectionObserver for lazy-loading embedded images.
|
||||
// Images are rendered with data-lazy-src; this observer sets the real src
|
||||
// when the image is within 200px of the visible scroll area.
|
||||
const lazyImgObserver = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
const img = entry.target;
|
||||
const src = img.dataset.lazySrc;
|
||||
if (src) { img.src = src; delete img.dataset.lazySrc; }
|
||||
lazyImgObserver.unobserve(img);
|
||||
}
|
||||
}, {
|
||||
rootMargin: '200px', // start loading 200px before entering viewport
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
function updateBadge() {
|
||||
const badge = document.getElementById('gchat-badge');
|
||||
const bubble = document.getElementById('gchat-reopen-bubble');
|
||||
@@ -187,7 +203,7 @@
|
||||
'gi'
|
||||
);
|
||||
html = html.replace(imageRegex, url =>
|
||||
`<span class="gchat-embed-img"><img src="${url}" loading="lazy" alt="" onerror="this.parentNode.style.display='none'"></span>`
|
||||
`<span class="gchat-embed-img"><img data-lazy-src="${url}" src="" loading="lazy" alt="" onerror="this.parentNode.style.display='none'"></span>`
|
||||
);
|
||||
|
||||
// 6b. Raw video URLs from allowed hosts → <video>
|
||||
@@ -270,7 +286,7 @@
|
||||
if (/\.(mp4|webm|ogv|mov)(\?.*)?$/i.test(path))
|
||||
return `${pre}<span class="gchat-embed-video"><video src="${url}" controls loop muted playsinline preload="metadata"></video></span>`;
|
||||
if (/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i.test(path))
|
||||
return `${pre}<span class="gchat-embed-img"><img src="${url}" loading="lazy" alt="" onerror="this.parentNode.style.display='none'"></span>`;
|
||||
return `${pre}<span class="gchat-embed-img"><img data-lazy-src="${url}" src="" loading="lazy" alt="" onerror="this.parentNode.style.display='none'"></span>`;
|
||||
if (/\.(mp3|ogg|wav|flac|aac|opus|m4a)(\?.*)?$/i.test(path))
|
||||
return `${pre}<span class="gchat-embed-audio"><audio src="${url}" controls preload="metadata"></audio></span>`;
|
||||
}
|
||||
@@ -310,14 +326,18 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function scrollToBottom(force = false) {
|
||||
function scrollToBottom(force = false, smooth = false) {
|
||||
const el = document.getElementById('gchat-messages');
|
||||
if (!el) return;
|
||||
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 120;
|
||||
if (!force && !nearBottom) return;
|
||||
// Double rAF ensures the browser has committed the layout before we measure scrollHeight
|
||||
if (smooth) {
|
||||
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
||||
} else {
|
||||
// Double rAF ensures layout is committed before reading scrollHeight
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the message container and keep snapping to bottom for durationMs.
|
||||
@@ -351,9 +371,15 @@
|
||||
};
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
// Debounce: batch rapid layout changes (e.g. progressive image renders)
|
||||
// into a single smooth scroll instead of many jarring instant jumps.
|
||||
let debounceTimer = null;
|
||||
ro = new ResizeObserver(() => {
|
||||
if (userScrolledUp) { cleanup(); return; }
|
||||
el.scrollTop = el.scrollHeight;
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
if (!userScrolledUp) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
||||
}, 80);
|
||||
});
|
||||
ro.observe(el);
|
||||
} else {
|
||||
@@ -364,10 +390,9 @@
|
||||
|
||||
setTimeout(cleanup, durationMs);
|
||||
|
||||
// Immediate snaps
|
||||
// First snap is instant (no animation — the panel just opened)
|
||||
scrollToBottom(true);
|
||||
setTimeout(() => { if (!userScrolledUp) scrollToBottom(true); }, 200);
|
||||
setTimeout(() => { if (!userScrolledUp) scrollToBottom(true); }, 600);
|
||||
setTimeout(() => { if (!userScrolledUp) scrollToBottom(true); }, 150);
|
||||
}
|
||||
|
||||
async function fetchYtOembed(cardEl) {
|
||||
@@ -470,8 +495,15 @@
|
||||
s.addEventListener('click', () => s.classList.toggle('revealed'));
|
||||
});
|
||||
|
||||
// Embedded images: scroll to bottom when loaded + open modal on click
|
||||
node.querySelectorAll('.gchat-embed-img img').forEach(img => {
|
||||
// Embedded images: register with lazy observer; scroll on load only for new messages (not history)
|
||||
node.querySelectorAll('.gchat-embed-img img[data-lazy-src]').forEach(img => {
|
||||
img.addEventListener('load', () => scrollToBottom(scrollForce));
|
||||
img.addEventListener('click', () => openImgModal(img.src));
|
||||
img.style.cursor = 'zoom-in';
|
||||
lazyImgObserver.observe(img);
|
||||
});
|
||||
// Also handle already-src'd images (avatars etc.)
|
||||
node.querySelectorAll('.gchat-embed-img img:not([data-lazy-src])').forEach(img => {
|
||||
img.addEventListener('load', () => scrollToBottom(scrollForce));
|
||||
img.addEventListener('click', () => openImgModal(img.src));
|
||||
img.style.cursor = 'zoom-in';
|
||||
@@ -498,10 +530,9 @@
|
||||
if (!data.success) return;
|
||||
const container = document.getElementById('gchat-messages');
|
||||
if (container) container.innerHTML = '';
|
||||
// Pass scrollForce=true so every img.load in the batch force-scrolls
|
||||
(data.messages || []).forEach(m => appendMsg(m, true));
|
||||
// Start sticky scroll: watches ResizeObserver + input events, holds 8s
|
||||
startStickyScroll(8000);
|
||||
(data.messages || []).forEach(m => appendMsg(m, false));
|
||||
// One instant snap — images above viewport won't load (lazy) so no layout shift
|
||||
container.scrollTop = container.scrollHeight;
|
||||
} catch (e) {
|
||||
console.error('[Chat] Failed to load history:', e);
|
||||
}
|
||||
|
||||
@@ -1201,7 +1201,9 @@ if (window.__dmLoaded) {
|
||||
bubble.appendChild(placeholder);
|
||||
}
|
||||
|
||||
// Auto-decrypt and render
|
||||
// Defer decrypt until the placeholder scrolls near the viewport.
|
||||
// This means attachments above the initial view never cause layout shifts.
|
||||
const decryptWhenVisible = () => {
|
||||
(async () => {
|
||||
const blob = await fetchAndDecryptAttachment(att.id, att.mime);
|
||||
if (!blob || !placeholder.parentNode) return;
|
||||
@@ -1264,6 +1266,16 @@ if (window.__dmLoaded) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(snapIfAtBottom));
|
||||
}
|
||||
})();
|
||||
}; // end decryptWhenVisible
|
||||
|
||||
// Use IntersectionObserver to defer decryption until near viewport.
|
||||
// Bottom messages (already visible) fire immediately; older ones wait.
|
||||
const io = new IntersectionObserver((entries, obs) => {
|
||||
if (!entries[0].isIntersecting) return;
|
||||
obs.disconnect();
|
||||
decryptWhenVisible();
|
||||
}, { rootMargin: '200px', threshold: 0 });
|
||||
io.observe(placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2481,9 +2493,13 @@ if (window.__dmLoaded) {
|
||||
|
||||
// ResizeObserver: re-snap whenever content grows (images, attachments decrypting)
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
let debounceTimer = null;
|
||||
ro = new ResizeObserver(() => {
|
||||
if (userScrolledUp) { cleanup(); return; }
|
||||
el.scrollTop = el.scrollHeight;
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
if (!userScrolledUp) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
||||
}, 80);
|
||||
});
|
||||
ro.observe(el);
|
||||
} else {
|
||||
@@ -2496,7 +2512,7 @@ if (window.__dmLoaded) {
|
||||
// Disconnect after the deadline regardless
|
||||
setTimeout(cleanup, durationMs);
|
||||
|
||||
// Immediate snaps as safety net for content already in the DOM
|
||||
// First snaps are instant (panel just opened — no animation needed)
|
||||
snapToBottom(el, true);
|
||||
setTimeout(() => { if (!userScrolledUp) snapToBottom(el, true); }, 200);
|
||||
setTimeout(() => { if (!userScrolledUp) snapToBottom(el, true); }, 600);
|
||||
|
||||
Reference in New Issue
Block a user