skeleleleleleeleltons

This commit is contained in:
2026-05-14 14:24:33 +02:00
parent db0c4cdc6c
commit 320ff03c81
4 changed files with 172 additions and 21 deletions

View File

@@ -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% {

View File

@@ -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 `<img src="${this.customEmojis[name]}" style="height:60px;vertical-align:middle;" alt="${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.');

View File

@@ -435,6 +435,42 @@
}
});
const SIDEBAR_SKELETON_COUNT = 15;
const showSkeletons = () => {
const container = document.getElementById('sidebar-activity-container');
if (!container) return;
const variants = [
`<div class="skeleton-line skeleton-text-long"></div>
<div class="skeleton-line skeleton-text-medium"></div>
<div class="skeleton-line skeleton-text-short"></div>`,
`<div class="skeleton-line skeleton-text-long"></div>
<div class="skeleton-line skeleton-text-short"></div>`,
`<div class="skeleton-line skeleton-text-medium"></div>
<div class="skeleton-line skeleton-text-long"></div>
<div class="skeleton-line skeleton-text-short"></div>`,
`<div class="skeleton-line skeleton-text-long"></div>
<div class="skeleton-line skeleton-text-medium"></div>`,
`<div class="skeleton-line skeleton-text-short"></div>
<div class="skeleton-line skeleton-text-long"></div>`,
];
let html = '';
for (let i = 0; i < SIDEBAR_SKELETON_COUNT; i++) {
html += `
<div class="sidebar-skeleton-item">
<div class="skeleton-header">
<div class="skeleton-avatar"></div>
<div class="skeleton-meta">
<div class="skeleton-line skeleton-name"></div>
<div class="skeleton-line skeleton-time"></div>
</div>
</div>
${variants[i % 5]}
</div>`;
}
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 = '<div class="loading">Loading activity...</div>';
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 = '<div style="text-align:center;padding:20px;color:#888;">No recent activity.</div>';
hasMore = false;
}
} catch (e) {
console.error("Sidebar Activity: Failed to load activity", e);
if (container.innerHTML.includes('loading')) {
if (!hasCache) {
container.innerHTML = '<div style="text-align:center;padding:20px;color:#888;">Failed to load.</div>';
}
hasMore = false;
@@ -499,6 +539,7 @@
}
};
const loadMoreActivity = async () => {
const container = document.getElementById('sidebar-activity-container');
if (!container || loading || loadingMore || !hasMore) return;

View File

@@ -106,7 +106,21 @@
<div class="global-sidebar-right">
<div class="sidebar-activity">
<div id="sidebar-activity-container" class="sidebar-comments-list">
<div class="loading">{{ t('sidebar.loading_activity') }}</div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-short"></div><div class="skeleton-line skeleton-text-long"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-short"></div><div class="skeleton-line skeleton-text-long"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-short"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-medium"></div><div class="skeleton-line skeleton-text-long"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-long"></div><div class="skeleton-line skeleton-text-medium"></div></div>
<div class="sidebar-skeleton-item"><div class="skeleton-header"><div class="skeleton-avatar"></div><div class="skeleton-meta"><div class="skeleton-line skeleton-name"></div><div class="skeleton-line skeleton-time"></div></div></div><div class="skeleton-line skeleton-text-short"></div><div class="skeleton-line skeleton-text-long"></div></div>
</div>
</div>
<div class="global-sidebar-right-footer">
@@ -402,6 +416,7 @@
sidebar_read_more: "{{ t('sidebar.read_more') }}",
sidebar_see_less: "{{ t('sidebar.see_less') }}",
sidebar_show_full_comment: "{{ t('sidebar.show_full_comment') }}",
sidebar_loading_activity: "{{ t('sidebar.loading_activity') }}",
select_file: "{{ t('upload_btn.select_file') }}",
enter_url: "{{ t('upload_btn.enter_url') }}",
tags_required: "{{ t('upload_btn.tags_required') }}",