Add loading state with braille spinner to slide-show more button

This commit is contained in:
Laan Tungir
2026-08-17 06:28:11 -04:00
parent ca38540cfa
commit 5e261023b2
2 changed files with 77 additions and 9 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.104",
"VERSION_NUMBER": "0.7.104",
"BUILD_DATE": "2026-08-17T10:13:32.771Z"
"VERSION": "v0.7.105",
"VERSION_NUMBER": "0.7.105",
"BUILD_DATE": "2026-08-17T10:28:11.271Z"
}
+74 -6
View File
@@ -215,6 +215,33 @@
filter: grayscale(var(--image-grayscale-hover));
}
#btnSlideMore.is-loading {
cursor: progress;
opacity: 0.85;
}
.braille-spinner {
display: inline-block;
width: 1.1em;
text-align: left;
overflow: hidden;
vertical-align: middle;
white-space: nowrap;
font-family: monospace;
letter-spacing: 0;
}
.braille-spinner::after {
content: "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
display: inline-block;
animation: braille-spin 0.9s steps(10) infinite;
}
@keyframes braille-spin {
0% { transform: translateX(0); }
100% { transform: translateX(-10em); }
}
@media (max-width: 640px) {
#divSlideGrid {
@@ -382,6 +409,7 @@ const versionInfo = await getVersion();
let notesSubscription = null;
let initialLoadSettled = false;
let initialLoadTimer = null;
let renderDebounceTimer = null;
const noteMap = new Map();
let imagePosts = [];
@@ -601,7 +629,18 @@ const versionInfo = await getVersion();
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
}
function setLoadingState(_isLoading) {}
function setLoadingState(isLoading) {
if (!btnSlideMore) return;
if (isLoading) {
btnSlideMore.classList.add('is-loading');
btnSlideMore.disabled = true;
btnSlideMore.innerHTML = '<span class="braille-spinner"></span> loading';
} else {
btnSlideMore.classList.remove('is-loading');
btnSlideMore.disabled = false;
updateMoreButton();
}
}
function getVisibleImagePosts() {
const visibleCount = Math.max(0, Math.min(visibleImageCount, imagePosts.length));
@@ -756,6 +795,19 @@ const versionInfo = await getVersion();
updateMoreButton();
}
// Debounce full re-renders triggered by live event updates so a burst of
// incoming events (e.g. the tail of a large subscription) doesn't clear and
// rebuild the grid on every single event, which causes scroll jank.
function scheduleGridRender() {
if (renderDebounceTimer) {
clearTimeout(renderDebounceTimer);
}
renderDebounceTimer = setTimeout(() => {
renderDebounceTimer = null;
renderGrid();
}, 250);
}
function appendMoreTiles() {
if (imagePosts.length === 0) return;
divSlideEmpty.style.display = 'none';
@@ -998,14 +1050,20 @@ const versionInfo = await getVersion();
const changed = upsertNoteIfImage(evt);
if (changed) {
rebuildImagePosts();
renderGrid();
setLoadingState(false);
if (initialLoadSettled) showLatestPublishedImageIfEnabled();
// During the initial load window we only accumulate events; the
// single render happens in settleInitialLoad() using the estimate.
// This avoids re-rendering (and re-laying-out) on every incoming
// event, which caused scroll jank and over-rendering.
if (initialLoadSettled) {
scheduleGridRender();
showLatestPublishedImageIfEnabled();
}
}
}
}
function startKind1Subscription() {
setLoadingState(true);
notesSubscription = subscribe(
{
kinds: [1],
@@ -1031,6 +1089,7 @@ const versionInfo = await getVersion();
return;
}
setLoadingState(true);
publicPool = new window.NostrTools.SimplePool();
publicSub = publicPool.subscribeMany(
PUBLIC_READ_RELAYS,
@@ -1040,8 +1099,12 @@ const versionInfo = await getVersion();
const changed = upsertNoteIfImage(evt);
if (changed) {
rebuildImagePosts();
renderGrid();
if (initialLoadSettled) showLatestPublishedImageIfEnabled();
// Same as handleNdkEvent: only render after the initial load
// settles to avoid scroll jank and over-rendering.
if (initialLoadSettled) {
scheduleGridRender();
showLatestPublishedImageIfEnabled();
}
}
},
oneose: () => {
@@ -1086,6 +1149,11 @@ const versionInfo = await getVersion();
clearTimeout(initialLoadTimer);
initialLoadTimer = null;
}
if (renderDebounceTimer) {
clearTimeout(renderDebounceTimer);
renderDebounceTimer = null;
}
}
(async function main() {