diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 6f691d3..46cdc6e 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -1992,6 +1992,79 @@ body.sidebar-right-hidden .global-sidebar-right { margin-bottom: 4px; } +/* ─── Sidebar Skeleton Loader ─────────────────────────────────────────────── */ + +@keyframes skeleton-shimmer { + 0% { background-position: -300px 0; } + 100% { background-position: 300px 0; } +} + +.sidebar-skeleton-item { + padding: 8px 5px 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.sidebar-skeleton-item:last-child { + border-bottom: none; +} + +.skeleton-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.skeleton-meta { + display: flex; + flex-direction: column; + gap: 5px; + flex: 1; + min-width: 0; +} + +/* Shared shimmer base */ +.skeleton-avatar, +.skeleton-line { + background: linear-gradient( + 90deg, + rgba(255,255,255,0.04) 0%, + rgba(255,255,255,0.10) 40%, + rgba(255,255,255,0.04) 80% + ); + background-size: 600px 100%; + animation: skeleton-shimmer 1.6s ease-in-out infinite; + border-radius: 3px; +} + +.skeleton-avatar { + width: 24px; + height: 24px; + flex-shrink: 0; + border-radius: 2px; +} + +.skeleton-line { + height: 10px; +} + +.skeleton-name { width: 55%; } +.skeleton-time { width: 35%; height: 8px; opacity: 0.65; } + +.skeleton-text-long { width: 90%; margin-bottom: 6px; } +.skeleton-text-medium { width: 70%; margin-bottom: 6px; } +.skeleton-text-short { width: 45%; margin-bottom: 0; } + +/* Stagger the shimmer phase so each item feels alive individually (cycles every 5) */ +.sidebar-skeleton-item:nth-child(5n+2) .skeleton-avatar, +.sidebar-skeleton-item:nth-child(5n+2) .skeleton-line { animation-delay: 0.2s; } +.sidebar-skeleton-item:nth-child(5n+3) .skeleton-avatar, +.sidebar-skeleton-item:nth-child(5n+3) .skeleton-line { animation-delay: 0.4s; } +.sidebar-skeleton-item:nth-child(5n+4) .skeleton-avatar, +.sidebar-skeleton-item:nth-child(5n+4) .skeleton-line { animation-delay: 0.6s; } +.sidebar-skeleton-item:nth-child(5n+5) .skeleton-avatar, +.sidebar-skeleton-item:nth-child(5n+5) .skeleton-line { animation-delay: 0.8s; } + /* Mobile Stacking for Legacy Mode (max-width: 999px) */ @media (max-width: 999px) { .item-layout-container { @@ -6223,6 +6296,23 @@ button#togglebg { animation: none; } +/* ─── Posts Grid Skeleton: shimmer on real items while thumbnail loads ─────── */ +div.posts > a.lazy-thumb:not(.loaded) { + background: linear-gradient( + 90deg, + rgba(255,255,255,0.04) 0%, + rgba(255,255,255,0.10) 40%, + rgba(255,255,255,0.04) 80% + ); + background-size: 600px 100%; + animation: skeleton-shimmer 1.6s ease-in-out infinite; +} + +/* Stagger shimmer phases so items feel alive individually */ +div.posts > a.lazy-thumb:not(.loaded):nth-child(3n+2) { animation-delay: 0.2s; } +div.posts > a.lazy-thumb:not(.loaded):nth-child(3n+3) { animation-delay: 0.4s; } + + /* Make individual item entry subtler and faster for infinite scroll */ @keyframes fadeInFX { 0% { diff --git a/public/s/js/comments.js b/public/s/js/comments.js index 824159d..ab36b3d 100644 --- a/public/s/js/comments.js +++ b/public/s/js/comments.js @@ -1,3 +1,8 @@ +// Safe wrapper — window.f0ckDebug may not be defined yet when this file first executes +// (comments.js loads before the footer script block that sets window.f0ckDebug). +// Calling through this helper defers the lookup to invocation time, not parse time. +const _f0ckDebug = (...args) => (typeof window.f0ckDebug === 'function' ? window.f0ckDebug(...args) : void 0); + class CommentSystem { constructor() { this.container = document.getElementById('comments-container'); @@ -62,11 +67,11 @@ class CommentSystem { return; } if (this.container.dataset.commentSystemInit) { - window.f0ckDebug('[CommentSystem] Already initialized for this container'); + _f0ckDebug('[CommentSystem] Already initialized for this container'); return; } this.container.dataset.commentSystemInit = 'true'; - window.f0ckDebug('[CommentSystem] Initializing for item:', this.itemId); + _f0ckDebug('[CommentSystem] Initializing for item:', this.itemId); this.loadComments(); this.setupGlobalListeners(); @@ -190,7 +195,7 @@ class CommentSystem { this.stabilizationObserver.disconnect(); } this.stopStabilization(); - window.f0ckDebug('[CommentSystem] Instance destroyed'); + _f0ckDebug('[CommentSystem] Instance destroyed'); } async loadEmojis() { @@ -212,7 +217,7 @@ class CommentSystem { this.customEmojis[e.name] = e.url; }); CommentSystem.emojiCache = this.customEmojis; - window.f0ckDebug('Loaded Emojis:', this.customEmojis); + _f0ckDebug('Loaded Emojis:', this.customEmojis); // Preload images to prevent NS Binding Aborted errors this.preloadEmojiImages(); @@ -265,7 +270,7 @@ class CommentSystem { // ... renderEmoji(match, name) { - // window.f0ckDebug('Rendering Emoji:', name, this.customEmojis ? this.customEmojis[name] : 'No list'); + // _f0ckDebug('Rendering Emoji:', name, this.customEmojis ? this.customEmojis[name] : 'No list'); if (this.customEmojis && this.customEmojis[name]) { return `${name}`; } @@ -374,7 +379,7 @@ class CommentSystem { // However, if we ARE currently loading comments from server, skip re-render. if (this.initialLoadDone === false && this.lastData.length === 0) { - window.f0ckDebug('[CommentSystem] Live comment skipped - initial comments load in progress.'); + _f0ckDebug('[CommentSystem] Live comment skipped - initial comments load in progress.'); return; } @@ -599,7 +604,7 @@ class CommentSystem { // 1. Early Bail-out: If data is bit-for-bit identical, do nothing. // This is the primary defense against tab-switch reloads when nothing changed. if (preserveScroll && this._isDeeplyIdentical(data.comments, data.user_id, data.is_subscribed)) { - window.f0ckDebug('[CommentSystem] Sync: Data identical, bailing early to protect media.'); + _f0ckDebug('[CommentSystem] Sync: Data identical, bailing early to protect media.'); this.restoreState(state); this.preservingScroll = false; return; @@ -607,7 +612,7 @@ class CommentSystem { // 2. Reconciliation: If data changed but we want to preserve media. if (preserveScroll && this.lastData && this.lastData.length > 0) { - window.f0ckDebug('[CommentSystem] Sync: Data changed, reconciling DOM.'); + _f0ckDebug('[CommentSystem] Sync: Data changed, reconciling DOM.'); this.reconcile(data.comments, data.user_id, data.is_subscribed); this.initialLoadDone = true; this.restoreState(state); @@ -715,7 +720,7 @@ class CommentSystem { this.startStabilization(id); } } else if (retries > 0) { - window.f0ckDebug(`[CommentSystem] Scroll target #c${id} not found, retrying... (${retries} left)`); + _f0ckDebug(`[CommentSystem] Scroll target #c${id} not found, retrying... (${retries} left)`); setTimeout(() => this.scrollToComment(id, retries - 1), 200); } }; @@ -739,7 +744,7 @@ class CommentSystem { const scrollKeys = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', ' ', 'Home', 'End']; if (!scrollKeys.includes(e.key)) return; } - window.f0ckDebug(`[CommentSystem] Stabilization aborted due to ${e.type}`); + _f0ckDebug(`[CommentSystem] Stabilization aborted due to ${e.type}`); this.isUserInteracting = true; this.stopStabilization(); }; @@ -762,7 +767,7 @@ class CommentSystem { // If it shifted more than 10px (e.g. media loaded), re-scroll if (diff > 10 && checks < maxChecks) { - window.f0ckDebug(`[CommentSystem] Layout shift detected (${Math.round(diff)}px), re-stabilizing scroll...`); + _f0ckDebug(`[CommentSystem] Layout shift detected (${Math.round(diff)}px), re-stabilizing scroll...`); this.scrollToComment(id, 0, true); lastTop = currentEl.getBoundingClientRect().top; } @@ -1236,7 +1241,7 @@ class CommentSystem { // Check for edits or state changes using robust data-attributes const contentEl = el.querySelector('.comment-content'); if (contentEl && contentEl.dataset.raw !== incoming.content) { - window.f0ckDebug(`[CommentSystem] Reconcile: Updating content for #c${id}`); + _f0ckDebug(`[CommentSystem] Reconcile: Updating content for #c${id}`); contentEl.innerHTML = this.renderCommentContent(incoming.content, incoming.id); contentEl.dataset.raw = incoming.content; } @@ -1260,7 +1265,7 @@ class CommentSystem { const idStr = String(c.id); if (document.getElementById('c' + idStr)) return; - window.f0ckDebug(`[CommentSystem] Reconcile: Injecting new flat comment #c${idStr}`); + _f0ckDebug(`[CommentSystem] Reconcile: Injecting new flat comment #c${idStr}`); const html = this.renderComment(c, currentUserId, false, true); const tmp = document.createElement('div'); tmp.innerHTML = html; @@ -1322,7 +1327,7 @@ class CommentSystem { let el = document.getElementById('c' + idStr); if (!el) { - window.f0ckDebug(`[CommentSystem] Reconcile: Injecting new comment #c${idStr}`); + _f0ckDebug(`[CommentSystem] Reconcile: Injecting new comment #c${idStr}`); const html = this.renderComment(c, currentUserId, isReply); const tmp = document.createElement('div'); tmp.innerHTML = html; @@ -1957,7 +1962,7 @@ class CommentSystem { } setupDelegatedEvents() { - window.f0ckDebug('[DEBUG] Setting up delegated events for container:', this.container); + _f0ckDebug('[DEBUG] Setting up delegated events for container:', this.container); if (!this.container) return; // Ctrl+Enter to submit comment @@ -1985,7 +1990,7 @@ class CommentSystem { // Single Click Listener for Everything this.container.addEventListener('click', async (e) => { - window.f0ckDebug('[DEBUG] Click on container:', e.target); + _f0ckDebug('[DEBUG] Click on container:', e.target); const target = e.target; @@ -2503,7 +2508,7 @@ class CommentSystem { retryCount++; // Randomized exponential backoff const delay = Math.min(1000 * Math.pow(1.5, retryCount) + (Math.random() * 1000), 10000); - window.f0ckDebug(`[CommentSystem] Retrying in ${Math.round(delay)}ms...`); + _f0ckDebug(`[CommentSystem] Retrying in ${Math.round(delay)}ms...`); setTimeout(attemptSubmit, delay); } else { alert('Failed to send comment after multiple attempts. Please check your connection.'); diff --git a/public/s/js/sidebar-activity.js b/public/s/js/sidebar-activity.js index a35ea8b..f29f2c1 100644 --- a/public/s/js/sidebar-activity.js +++ b/public/s/js/sidebar-activity.js @@ -435,6 +435,42 @@ } }); + const SIDEBAR_SKELETON_COUNT = 15; + + const showSkeletons = () => { + const container = document.getElementById('sidebar-activity-container'); + if (!container) return; + const variants = [ + `
+
+
`, + `
+
`, + `
+
+
`, + `
+
`, + `
+
`, + ]; + let html = ''; + for (let i = 0; i < SIDEBAR_SKELETON_COUNT; i++) { + html += ` + `; + } + container.innerHTML = html; + }; + const renderFromCache = () => { const container = document.getElementById('sidebar-activity-container'); if (!container || window._sidebarActivityCache.length === 0) return false; @@ -461,10 +497,14 @@ if (!container || loading) return; const hasCache = renderFromCache(); + // If no cache and not silent: show skeletons while we fetch. + // On the very first page load the server-rendered skeletons are already there; + // on subsequent loads (e.g. mode change) we inject them programmatically. if (!hasCache && !silent) { - container.innerHTML = '
Loading activity...
'; + showSkeletons(); } + loading = true; currentPage = 1; hasMore = true; @@ -484,13 +524,13 @@ renderFromCache(); // Also check after a delay to account for image/emoji loading shifts setTimeout(checkOverflow, 500); - } else if (container.innerHTML.includes('loading')) { + } else if (!hasCache) { container.innerHTML = '
No recent activity.
'; hasMore = false; } } catch (e) { console.error("Sidebar Activity: Failed to load activity", e); - if (container.innerHTML.includes('loading')) { + if (!hasCache) { container.innerHTML = '
Failed to load.
'; } hasMore = false; @@ -499,6 +539,7 @@ } }; + const loadMoreActivity = async () => { const container = document.getElementById('sidebar-activity-container'); if (!container || loading || loadingMore || !hasMore) return; diff --git a/views/snippets/footer.html b/views/snippets/footer.html index 74fbefb..084b02c 100644 --- a/views/snippets/footer.html +++ b/views/snippets/footer.html @@ -106,7 +106,21 @@