// 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 { get isMainSubmitting() { return this._globalState ? this._globalState.isMainSubmitting : false; } set isMainSubmitting(val) { if (this._globalState) this._globalState.isMainSubmitting = val; } get pendingSubmissions() { return this._globalState ? this._globalState.pendingSubmissions : new Set(); } 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; // Retrieve or initialize global submission state for this item to survive f0ck:contentLoaded re-inits if (this.itemId) { window._f0ckActiveSubmissions = window._f0ckActiveSubmissions || {}; if (!window._f0ckActiveSubmissions[this.itemId]) { window._f0ckActiveSubmissions[this.itemId] = { isMainSubmitting: false, pendingSubmissions: new Set() }; } this._globalState = window._f0ckActiveSubmissions[this.itemId]; } else { this._globalState = { isMainSubmitting: false, pendingSubmissions: new Set() }; } this.scrollListenerAdded = false; this.commentCache = new Map(); this._anchorScrollDone = false; // true after the first hash-anchor scroll on initial load // Defer emoji loading — only fetch if the page actually has :emoji: patterns // (avoids a full API round-trip on every item page that has no custom emojis) this._emojiLoadScheduled = false; 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) { _f0ckDebug('[CommentSystem] Already initialized for this container'); return; } this.container.dataset.commentSystemInit = 'true'; _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