Increase slide-show subscription limit to 10000 and improve image loading
This commit is contained in:
+7
-3
@@ -364,18 +364,22 @@ a.nostr-embed-preview-text:hover {
|
||||
}
|
||||
|
||||
#divBody {
|
||||
position: relative;
|
||||
position: fixed;
|
||||
top: max(var(--header-height), var(--header-min-height));
|
||||
bottom: max(var(--footer-height), var(--footer-min-height));
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--background-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
align-content: space-around;
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
width: auto;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#divFooter {
|
||||
|
||||
@@ -743,18 +743,20 @@ function appendTaggedMedia(contentEl, eventData) {
|
||||
if (!media.video.length) return;
|
||||
|
||||
const existingVideoSrcs = new Set(
|
||||
Array.from(contentEl.querySelectorAll('video')).map((el) => el.getAttribute('src')).filter(Boolean)
|
||||
Array.from(contentEl.querySelectorAll('video')).map((el) => el.getAttribute('src') || el.getAttribute('data-src')).filter(Boolean)
|
||||
);
|
||||
|
||||
media.video.forEach((url) => {
|
||||
if (existingVideoSrcs.has(url)) return;
|
||||
const videoEl = document.createElement('video');
|
||||
videoEl.className = 'post-video-embed';
|
||||
videoEl.src = url;
|
||||
videoEl.controls = true;
|
||||
videoEl.playsInline = true;
|
||||
videoEl.preload = 'metadata';
|
||||
videoEl.preload = 'none';
|
||||
videoEl.setAttribute('data-src', url);
|
||||
contentEl.appendChild(videoEl);
|
||||
/* The global IntersectionObserver in utilities.mjs will promote
|
||||
* data-src to src when this video scrolls into view. */
|
||||
});
|
||||
}
|
||||
|
||||
@@ -768,13 +770,25 @@ function applyVideoPlaybackPreferences(contentEl, autoplayVideo = false) {
|
||||
videoEl.controls = true;
|
||||
videoEl.playsInline = true;
|
||||
videoEl.setAttribute('playsinline', '');
|
||||
videoEl.preload = 'metadata';
|
||||
|
||||
/* Keep preload='none' for lazy-loaded videos (data-src not yet
|
||||
* promoted to src). Once src is set, allow metadata preload. */
|
||||
if (videoEl.getAttribute('data-src') && !videoEl.src) {
|
||||
videoEl.preload = 'none';
|
||||
} else {
|
||||
videoEl.preload = 'metadata';
|
||||
}
|
||||
|
||||
if (autoplayVideo) {
|
||||
videoEl.autoplay = true;
|
||||
videoEl.muted = true;
|
||||
videoEl.setAttribute('autoplay', '');
|
||||
videoEl.setAttribute('muted', '');
|
||||
/* If lazy-loaded and not yet visible, force-load now for autoplay. */
|
||||
const dataSrc = videoEl.getAttribute('data-src');
|
||||
if (dataSrc && !videoEl.src) {
|
||||
videoEl.src = dataSrc;
|
||||
}
|
||||
const playPromise = videoEl.play?.();
|
||||
if (playPromise?.catch) playPromise.catch(() => {});
|
||||
} else {
|
||||
|
||||
+68
-1
@@ -27,6 +27,69 @@ export const objMimeTypes = {
|
||||
|
||||
export const arrImageExt = [".apng", ".avif", ".gif", ".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", ".png", ".svg", ".webp"];
|
||||
|
||||
/* ── Global lazy-load for <video data-src="..."> elements ──────────────
|
||||
* WebKitGTK spins up a full GStreamer pipeline (~20+ threads) per <video>
|
||||
* as soon as `src` is set, even with preload='metadata'. A feed with 11
|
||||
* videos creates 250+ threads in a single WebKitWebProcess. To avoid this,
|
||||
* videos are created with `data-src` instead of `src`. This global
|
||||
* IntersectionObserver + MutationObserver promotes `data-src` to `src`
|
||||
* only when the video scrolls near the viewport. */
|
||||
const _lazyVideoIO = (typeof IntersectionObserver !== 'undefined')
|
||||
? new IntersectionObserver(
|
||||
(entries, obs) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
const el = entry.target;
|
||||
const url = el.getAttribute('data-src');
|
||||
if (url && !el.src) {
|
||||
el.preload = 'metadata';
|
||||
el.src = url;
|
||||
}
|
||||
obs.unobserve(el);
|
||||
});
|
||||
},
|
||||
{ rootMargin: '300px', threshold: 0 }
|
||||
)
|
||||
: null;
|
||||
|
||||
if (typeof document !== 'undefined' && _lazyVideoIO) {
|
||||
/* Observe existing videos with data-src */
|
||||
document.querySelectorAll('video[data-src]').forEach((v) => _lazyVideoIO.observe(v));
|
||||
|
||||
/* Watch for new videos added to the DOM */
|
||||
const _lazyVideoMO = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType !== 1) return;
|
||||
const videos = (node.tagName === 'VIDEO')
|
||||
? [node]
|
||||
: (node.querySelectorAll ? Array.from(node.querySelectorAll('video[data-src]')) : []);
|
||||
videos.forEach((v) => {
|
||||
if (v.hasAttribute('data-src') && !v.src) {
|
||||
/* Delay observation until after layout to avoid
|
||||
* false-positive intersection at (0,0). */
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (v.hasAttribute('data-src') && !v.src) {
|
||||
_lazyVideoIO.observe(v);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
if (document.body) {
|
||||
_lazyVideoMO.observe(document.body, { childList: true, subtree: true });
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
_lazyVideoMO.observe(document.body, { childList: true, subtree: true });
|
||||
document.querySelectorAll('video[data-src]').forEach((v) => _lazyVideoIO.observe(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const htmlCenteringWrapper = `<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
@@ -645,9 +708,13 @@ export const htmlFormatText = (txt) => {
|
||||
if (rgxImg.test(EachLink)) {
|
||||
chunk = chunk.replace(EachLink, `<img src="${EachLink}">`);
|
||||
} else if (isVideoUrl(EachLink)) {
|
||||
/* Lazy-load: use data-src instead of src to prevent WebKitGTK
|
||||
* from spinning up GStreamer pipelines for off-screen videos.
|
||||
* The global IntersectionObserver in utilities.mjs promotes
|
||||
* data-src to src when the video scrolls into view. */
|
||||
chunk = chunk.replace(
|
||||
EachLink,
|
||||
`<video class="post-video-embed" src="${EachLink}" controls playsinline preload="metadata"></video>`
|
||||
`<video class="post-video-embed" data-src="${EachLink}" controls playsinline preload="none"></video>`
|
||||
);
|
||||
} else {
|
||||
chunk = chunk.replace(EachLink, `<a href="${EachLink}" target="_blank" rel="noopener noreferrer">${EachLink}</a>`);
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.7.103",
|
||||
"VERSION_NUMBER": "0.7.103",
|
||||
"BUILD_DATE": "2026-08-08T11:27:48.503Z"
|
||||
"VERSION": "v0.7.104",
|
||||
"VERSION_NUMBER": "0.7.104",
|
||||
"BUILD_DATE": "2026-08-17T10:13:32.771Z"
|
||||
}
|
||||
|
||||
+107
-25
@@ -393,9 +393,7 @@ const versionInfo = await getVersion();
|
||||
|
||||
const SLIDESHOW_SECONDS_STORAGE_KEY = 'slideShowSeconds';
|
||||
const SLIDESHOW_MODE_STORAGE_KEY = 'slideShowMode';
|
||||
const INITIAL_VISIBLE_IMAGE_COUNT = 200;
|
||||
const MORE_IMAGES_CHUNK = 200;
|
||||
let visibleImageCount = INITIAL_VISIBLE_IMAGE_COUNT;
|
||||
let visibleImageCount = 200; // updated to screen-fit estimate at load time
|
||||
|
||||
const PUBLIC_READ_RELAYS = [
|
||||
'wss://relay.damus.io',
|
||||
@@ -668,6 +666,67 @@ const versionInfo = await getVersion();
|
||||
return Math.max(1, Math.floor(width / minTileWidth));
|
||||
}
|
||||
|
||||
// Estimated width/height ratio of a typical image tile. 1.0 (square)
|
||||
// is a reasonable middle-ground for photos, screenshots and memes.
|
||||
const ESTIMATED_IMAGE_ASPECT_RATIO = 1.0;
|
||||
// Show more than the strict fit so the masonry grid still looks full
|
||||
// once real (varying) image heights are loaded.
|
||||
const SCREEN_FILL_BUFFER = 2.5;
|
||||
|
||||
function estimateVisibleImageCount() {
|
||||
const viewportWidth = window.innerWidth || 1024;
|
||||
const viewportHeight = window.innerHeight || 768;
|
||||
|
||||
let headerHeight = 60;
|
||||
let footerHeight = 20;
|
||||
try {
|
||||
const rootStyle = getComputedStyle(document.documentElement);
|
||||
headerHeight = Math.max(
|
||||
parseFloat(rootStyle.getPropertyValue('--header-min-height')) || 60,
|
||||
parseFloat(rootStyle.getPropertyValue('--header-height')) || 0
|
||||
);
|
||||
footerHeight = Math.max(
|
||||
parseFloat(rootStyle.getPropertyValue('--footer-min-height')) || 20,
|
||||
parseFloat(rootStyle.getPropertyValue('--footer-height')) || 0
|
||||
);
|
||||
} catch (_error) {
|
||||
// keep default header/footer estimates
|
||||
}
|
||||
|
||||
const availableHeight = Math.max(200, viewportHeight - headerHeight - footerHeight);
|
||||
const columnCount = getSlideColumnCount();
|
||||
const gridWidth = divSlideGrid?.clientWidth || viewportWidth;
|
||||
const tileWidth = Math.max(80, gridWidth / columnCount);
|
||||
const tileHeight = tileWidth / ESTIMATED_IMAGE_ASPECT_RATIO + 12; // include gap/margin
|
||||
const rowsThatFit = Math.max(1, Math.floor(availableHeight / tileHeight));
|
||||
const estimatedCount = Math.ceil(columnCount * rowsThatFit * SCREEN_FILL_BUFFER);
|
||||
return Math.max(columnCount, estimatedCount);
|
||||
}
|
||||
|
||||
function createTileElements(posts, startIndex, endIndex, totalVisible) {
|
||||
const columnCount = getSlideColumnCount();
|
||||
const tiles = [];
|
||||
for (let i = startIndex; i < endIndex; i += 1) {
|
||||
const post = posts[i];
|
||||
const tile = document.createElement('button');
|
||||
tile.className = 'slideTile';
|
||||
tile.type = 'button';
|
||||
tile.dataset.index = String(i);
|
||||
tile.title = `Open image ${i + 1} of ${totalVisible}`;
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = post.imageUrl;
|
||||
img.alt = 'Post image';
|
||||
img.loading = 'lazy';
|
||||
|
||||
tile.appendChild(img);
|
||||
tile.addEventListener('click', () => openLightbox(i));
|
||||
|
||||
tiles.push({ tile, columnIndex: i % columnCount });
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
divSlideGrid.innerHTML = '';
|
||||
if (imagePosts.length === 0) {
|
||||
@@ -678,8 +737,6 @@ const versionInfo = await getVersion();
|
||||
|
||||
divSlideEmpty.style.display = 'none';
|
||||
|
||||
const visiblePosts = getVisibleImagePosts();
|
||||
const visibleTotal = visiblePosts.length;
|
||||
const columnCount = getSlideColumnCount();
|
||||
const columns = [];
|
||||
for (let i = 0; i < columnCount; i += 1) {
|
||||
@@ -689,25 +746,41 @@ const versionInfo = await getVersion();
|
||||
divSlideGrid.appendChild(col);
|
||||
}
|
||||
|
||||
// Row-major visual ordering:
|
||||
// 0->col0, 1->col1, ... n->col(n%columnCount)
|
||||
visiblePosts.forEach((post, index) => {
|
||||
const tile = document.createElement('button');
|
||||
tile.className = 'slideTile';
|
||||
tile.type = 'button';
|
||||
tile.dataset.index = String(index);
|
||||
tile.title = `Open image ${index + 1} of ${visibleTotal}`;
|
||||
const visiblePosts = getVisibleImagePosts();
|
||||
const visibleTotal = visiblePosts.length;
|
||||
const tileEntries = createTileElements(visiblePosts, 0, visibleTotal, visibleTotal);
|
||||
tileEntries.forEach(({ tile, columnIndex }) => {
|
||||
columns[columnIndex].appendChild(tile);
|
||||
});
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = post.imageUrl;
|
||||
img.alt = 'Post image';
|
||||
img.loading = 'lazy';
|
||||
updateMoreButton();
|
||||
}
|
||||
|
||||
tile.appendChild(img);
|
||||
tile.addEventListener('click', () => openLightbox(index));
|
||||
function appendMoreTiles() {
|
||||
if (imagePosts.length === 0) return;
|
||||
divSlideEmpty.style.display = 'none';
|
||||
|
||||
const targetColumn = columns[index % columnCount];
|
||||
targetColumn.appendChild(tile);
|
||||
const existingColumns = divSlideGrid.querySelectorAll('.slideColumn');
|
||||
if (existingColumns.length === 0) {
|
||||
renderGrid();
|
||||
return;
|
||||
}
|
||||
|
||||
// Count how many tiles already exist across all columns
|
||||
let existingTileCount = 0;
|
||||
existingColumns.forEach((col) => { existingTileCount += col.children.length; });
|
||||
|
||||
const visiblePosts = getVisibleImagePosts();
|
||||
const newTotal = visiblePosts.length;
|
||||
|
||||
if (existingTileCount >= newTotal) {
|
||||
updateMoreButton();
|
||||
return;
|
||||
}
|
||||
|
||||
const tileEntries = createTileElements(visiblePosts, existingTileCount, newTotal, newTotal);
|
||||
tileEntries.forEach(({ tile, columnIndex }) => {
|
||||
existingColumns[columnIndex].appendChild(tile);
|
||||
});
|
||||
|
||||
updateMoreButton();
|
||||
@@ -878,8 +951,14 @@ const versionInfo = await getVersion();
|
||||
}
|
||||
}
|
||||
|
||||
function isReplyEvent(evt) {
|
||||
if (!evt?.tags || !Array.isArray(evt.tags)) return false;
|
||||
return evt.tags.some(tag => Array.isArray(tag) && tag[0] === 'e');
|
||||
}
|
||||
|
||||
function upsertNoteIfImage(evt) {
|
||||
if (!evt?.id || evt.kind !== 1 || evt.pubkey !== targetPubkey) return false;
|
||||
if (isReplyEvent(evt)) return false;
|
||||
const imageUrl = extractFirstDirectImageUrl(evt.content);
|
||||
if (!imageUrl) return false;
|
||||
|
||||
@@ -905,6 +984,9 @@ const versionInfo = await getVersion();
|
||||
initialLoadTimer = null;
|
||||
}
|
||||
setLoadingState(false);
|
||||
// Estimate how many tiles the viewport can hold so we fill the screen
|
||||
// without loading more than needed.
|
||||
visibleImageCount = estimateVisibleImageCount();
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
@@ -928,7 +1010,7 @@ const versionInfo = await getVersion();
|
||||
{
|
||||
kinds: [1],
|
||||
authors: [targetPubkey],
|
||||
limit: 2000
|
||||
limit: 10000
|
||||
},
|
||||
{
|
||||
closeOnEose: false,
|
||||
@@ -952,7 +1034,7 @@ const versionInfo = await getVersion();
|
||||
publicPool = new window.NostrTools.SimplePool();
|
||||
publicSub = publicPool.subscribeMany(
|
||||
PUBLIC_READ_RELAYS,
|
||||
[{ kinds: [1], authors: [targetPubkey], limit: 2000 }],
|
||||
[{ kinds: [1], authors: [targetPubkey], limit: 10000 }],
|
||||
{
|
||||
onevent: (evt) => {
|
||||
const changed = upsertNoteIfImage(evt);
|
||||
@@ -1077,8 +1159,8 @@ const versionInfo = await getVersion();
|
||||
|
||||
if (btnSlideMore) {
|
||||
btnSlideMore.addEventListener('click', () => {
|
||||
visibleImageCount += MORE_IMAGES_CHUNK;
|
||||
renderGrid();
|
||||
visibleImageCount += estimateVisibleImageCount();
|
||||
appendMoreTiles();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user