class CommentSystem { constructor() { this.container = document.getElementById('comments-container'); this.itemId = this.container ? this.container.dataset.itemId : null; this.user = this.container ? this.container.dataset.user : null; // logged in user? this.isAdmin = this.container ? this.container.dataset.isAdmin === 'true' : false; this.isLocked = this.container ? this.container.dataset.isLocked === 'true' : false; this.displayMode = window.f0ckSession?.comment_display_mode || 0; // 0=Tree, 1=Linear this.sort = (document.body.classList.contains('layout-legacy') || document.body.classList.contains('legacy-view')) ? 'old' : 'new'; // Linear mode usually implies chronological order (4chan style) if (this.displayMode === 1) this.sort = 'old'; this.customEmojis = CommentSystem.emojiCache || {}; this.icons = { reply: ``, pin: ``, unpin: ``, edit: ``, delete: ``, link: ``, pinned: ``, lock: ``, unlock: `` }; // Restore visibility state if (this.container) { const isHidden = localStorage.getItem('comments_hidden') === 'true'; // Force show if hash is present if (window.location.hash && window.location.hash.startsWith('#c')) { this.container.classList.remove('faded-out'); this.container.style.display = 'block'; localStorage.setItem('comments_hidden', 'false'); } else if (isHidden) { this.container.classList.add('faded-out'); this.container.style.display = 'none'; const layout = this.container.closest('.item-layout-container'); if (layout) layout.classList.add('sidebar-hidden'); } } this.initialLoadDone = false; this.pendingSubmissions = new Set(); this.isMainSubmitting = false; this.scrollListenerAdded = false; this.commentCache = new Map(); this._anchorScrollDone = false; // true after the first hash-anchor scroll on initial load this.loadEmojis(); // Always load emojis for previews this.setupHoverPreviews(); if (this.itemId) { this.init(); } } async init() { if (!this.container) { console.warn('[CommentSystem] Container not found during init'); return; } if (this.container.dataset.commentSystemInit) { window.f0ckDebug('[CommentSystem] Already initialized for this container'); return; } this.container.dataset.commentSystemInit = 'true'; window.f0ckDebug('[CommentSystem] Initializing for item:', this.itemId); this.loadComments(); this.setupGlobalListeners(); this.setupDelegatedEvents(); this.startLiveTimestamps(); this.setupScrollListener(); } setupScrollListener() { if (this.scrollListenerAdded) return; if (!document.body.classList.contains('layout-legacy') && !document.body.classList.contains('legacy-view')) return; const updateBtn = () => { const btn = document.querySelector('.scroll-to-bottom'); if (!btn) return; const container = document.getElementById('comments-container'); if (!container) return; const rect = container.getBoundingClientRect(); // Detect if we are at the bottom of the comments section const isAtBottom = rect.bottom < window.innerHeight + 100; if (isAtBottom) { btn.classList.add('is-at-bottom'); btn.setAttribute('title', 'Scroll to top'); } else { btn.classList.remove('is-at-bottom'); btn.setAttribute('title', 'Scroll to bottom'); } }; this.scrollHandler = updateBtn; window.addEventListener('scroll', this.scrollHandler, { passive: true }); this.scrollListenerAdded = true; // Dynamic detection after potential renders this.scrollTimer = setInterval(this.scrollHandler, 1000); } /** * Boot or reload the Danmaku engine with fresh comment data. * Called after render() so the overlay is always in sync with visible comments. */ _loadDanmaku(comments) { if (typeof Danmaku === 'undefined') return; const playerEl = document.querySelector('.v0ck') || document.getElementById('ruffle-container'); if (!playerEl) return; // Ensure the container is positioned so the absolute overlay works if (getComputedStyle(playerEl).position === 'static') { playerEl.style.position = 'relative'; } // Prefer real