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 ``;
}
@@ -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 = [
+ `