// 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 // The body class 'sidebar-left-hidden' is already set by the blocking // inline script in header.html (prevents flash). Here we sync the // container-level class and display state to match. 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'); document.body.classList.remove('sidebar-left-hidden'); const layout = this.container.closest('.item-layout-container'); if (layout) layout.classList.remove('sidebar-hidden'); } else if (isHidden && (window.innerWidth >= 1000 || !document.body.classList.contains('layout-modern'))) { this.container.classList.add('faded-out'); this.container.style.display = 'none'; document.body.classList.add('sidebar-left-hidden'); 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 currentlyAtBottom = btn.classList.contains('is-at-bottom'); 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'); if (bottomElement) { const bottomRect = bottomElement.getBoundingClientRect(); if (currentlyAtBottom) { // We are pointing UP. Switch to DOWN if we scroll significantly UP away from bottomElement if (bottomRect.top > window.innerHeight + 250) { isAtBottom = false; } } else { // We are pointing DOWN. Switch to UP if bottomElement enters viewport or we scroll past it if (bottomRect.top <= window.innerHeight + 50) { isAtBottom = true; } } } else { // Fallback to container rect if no bottom element const rect = container.getBoundingClientRect(); if (currentlyAtBottom) { if (rect.bottom > window.innerHeight + 250) { isAtBottom = false; } } else { if (rect.bottom <= window.innerHeight + 50) { isAtBottom = true; } } } if (isAtBottom && !currentlyAtBottom) { btn.classList.add('is-at-bottom'); btn.setAttribute('title', 'Scroll to top'); } else if (!isAtBottom && currentlyAtBottom) { 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