This commit is contained in:
2026-09-28 23:24:50 +02:00
parent 2cc768f1fd
commit 73c0659d4a
29 changed files with 820 additions and 117 deletions
+35 -8
View File
@@ -144,7 +144,7 @@ class CommentSystem {
let isAtBottom = currentlyAtBottom;
// Base the bottom state primarily on whether the input box / end-of-thread marker is visible
const bottomElement = container.querySelector('.main-input') || container.querySelector('.lock-notice') || container.querySelector('.login-placeholder');
const bottomElement = container.querySelector('.main-input') || container.querySelector('.lock-notice');
if (bottomElement) {
const bottomRect = bottomElement.getBoundingClientRect();
@@ -790,18 +790,25 @@ class CommentSystem {
}
}
// Render skeleton (Result: Layout visible immediately)
// Render skeleton only if the fetch is slow. Rendering it immediately made every item switch flash an
// empty list that the real comments then replaced a moment later.
// Skip when preserveScroll=true (tab re-focus refresh): the user already sees comments,
// so wiping the DOM causes the browser to lose the #c anchor element and auto-scroll to top.
let skeletonTimer = null;
if (!scrollToId && !preserveScroll) {
if (this.user || !this.container.querySelector('.login-placeholder, .comment-input, .lock-notice')) {
this.render([], this.user, initialIsSubscribed);
this.restoreState(state);
if (this.user || !this.container.querySelector('.comment-input, .lock-notice')) {
skeletonTimer = setTimeout(() => {
skeletonTimer = null;
this.render([], this.user, initialIsSubscribed);
this.restoreState(state);
}, 250);
}
}
const cancelSkeleton = () => { if (skeletonTimer) { clearTimeout(skeletonTimer); skeletonTimer = null; } };
try {
const res = await fetch(`/api/comments/${this.itemId}?sort=${this.sort}`);
cancelSkeleton();
// If server is restarting (502/503), res.ok will be false.
if (!res.ok) throw new Error(`Server returned ${res.status}`);
@@ -914,6 +921,7 @@ class CommentSystem {
}
}
} catch (e) {
cancelSkeleton();
console.error('[CommentSystem] Error loading comments:', e);
// Catch-all for network errors, 502s, JSON parse errors (e.g. server restart)
// If initial load already finished (SSR or first fetch), just keep existing comments on screen.
@@ -1389,7 +1397,8 @@ class CommentSystem {
} else if (currentUserId) {
inputSection = this.renderInput();
} else {
inputSection = '<div class="login-placeholder"></div>';
// Guests can read but not comment: an inert look-alike keeps the layout identical
inputSection = this.renderGuestInput();
}
const isLegacy = document.body.classList.contains('layout-legacy') || document.body.classList.contains('legacy-view');
@@ -1476,7 +1485,7 @@ class CommentSystem {
});
}
const mainInput = this.container.querySelector('.main-input');
const mainInput = this.container.querySelector('.main-input:not(.is-guest)');
if (mainInput) this.setupEmojiPicker(mainInput);
// Lazy emoji load: scan the just-rendered comment text for :emoji: patterns.
@@ -2383,6 +2392,24 @@ class CommentSystem {
</div>`;
}
// Same markup as renderInput() so every theme styles it identically, but inert: readonly, not focusable,
// no pointer events on the controls, no submit. Clicking it opens the login modal.
renderGuestInput() {
const i18n = window.f0ckI18n || {};
const placeholder = i18n.login_to_comment || 'Log in to comment';
const postLabel = i18n.post || 'Post';
return `
<div class="comment-input main-input is-guest" aria-disabled="true" title="${placeholder}"
onclick="const b=document.getElementById('nav-login-btn'); if (b) b.click();">
<textarea placeholder="${placeholder}" readonly tabindex="-1" aria-disabled="true"></textarea>
<div class="comment-file-preview"></div>
<div class="input-actions">
<button class="submit-comment" type="button" tabindex="-1" aria-disabled="true"><span class="submit-label">${postLabel}</span><i class="fa-solid fa-spinner fa-spin submit-spinner"></i></button>
</div>
</div>
`;
}
renderInput(parentId = null) {
const i18n = window.f0ckI18n || {};
const session = window.f0ckSession || {};
@@ -2742,7 +2769,7 @@ class CommentSystem {
});
} else {
// Scroll to Bottom of comments
const bottomElement = this.container.querySelector('.main-input') || this.container.querySelector('.lock-notice') || this.container.querySelector('.login-placeholder');
const bottomElement = this.container.querySelector('.main-input') || this.container.querySelector('.lock-notice');
if (bottomElement) {
bottomElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {