(function () { let customEmojis = {}; let loading = false; let loadingMore = false; let currentPage = 1; let hasMore = true; let ioSentinel = null; // persistent sentinel element for IntersectionObserver // Shared cache for activity across AJAX loads if (!window._sidebarActivityCache) window._sidebarActivityCache = []; const loadEmojis = async () => { if (Object.keys(customEmojis).length > 0) return; try { const res = await fetch('/api/v2/emojis'); const data = await res.json(); if (data.success) { data.emojis.forEach(e => { customEmojis[e.name] = e.url; }); } } catch (e) { console.error("Sidebar Activity: Failed to load emojis", e); } }; const renderEmoji = (match, name) => { if (customEmojis[name]) { return `${name}`; } return match; }; const escapeHtml = (unsafe) => { if (!unsafe) return ''; const div = document.createElement('div'); div.textContent = unsafe; return div.innerHTML; }; const ytOembedCache = new Map(); // videoId -> meta object const ytOembedPending = new Map(); // videoId -> Promise const fetchSidebarYoutubeTitles = async (container) => { const links = container.querySelectorAll('.sidebar-video-link[data-yt-id]'); if (links.length === 0) return; for (const link of links) { const videoId = link.dataset.ytId; if (!videoId) continue; const titleSpan = link.querySelector('.yt-title'); if (!titleSpan || titleSpan.dataset.loaded === 'true') continue; let meta = ytOembedCache.get(videoId); if (!meta) { if (ytOembedPending.has(videoId)) { meta = await ytOembedPending.get(videoId); } else { const promise = (async () => { try { const ytUrl = `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`; const r = await fetch(`/api/v2/meta/fetch?url=${encodeURIComponent(ytUrl)}`); if (r.ok) { const data = await r.json(); if (data.success && data.meta) { ytOembedCache.set(videoId, data.meta); return data.meta; } } } catch (e) { } return null; })(); ytOembedPending.set(videoId, promise); meta = await promise; ytOembedPending.delete(videoId); } } if (meta && meta.title) { titleSpan.textContent = meta.title; } else { // If title fails, just leave it blank or use a generic label titleSpan.textContent = 'YouTube Video'; } titleSpan.dataset.loaded = 'true'; } }; // Maximum characters to render in the sidebar per comment const SIDEBAR_CONTENT_TRUNCATE = 200; const renderCommentContent = (content, commentId = null, itemId = null) => { if (!content) return ''; // Truncate extremely long comments before any processing to keep the sidebar // fast and the DOM lean, regardless of markdown / regex complexity. if (content.length > SIDEBAR_CONTENT_TRUNCATE) { content = content.substring(0, SIDEBAR_CONTENT_TRUNCATE) + '\u2026'; } if (typeof marked === 'undefined') { return escapeHtml(content) .replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n)); } try { // Extract and protect code blocks (```...```) before escaping const codeBlocks = []; let processed = content.replace(/```([\s\S]*?)```/g, (match) => { const placeholder = `BLOCKPORTALX${codeBlocks.length}X`; codeBlocks.push(marked.parse(match)); return placeholder; }); let escaped = escapeHtml(processed) .replace(/>/g, ">"); // Restore > for markdown markers // Handle Image Embeds (Client-side) const siteOrigin = window.location.origin; const escapedSiteUrl = siteOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const allowedHosts = [escapedSiteUrl]; if (window.f0ckAllowedImages && Array.isArray(window.f0ckAllowedImages)) { window.f0ckAllowedImages.forEach(h => { const escapedHost = h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); allowedHosts.push(`(?:[a-z0-9-]+\\.)*${escapedHost}`); }); } const hostsRegexPart = allowedHosts.join('|'); // "Safe non-whitespace" stops at protocol boundaries so concatenated URLs aren't merged. const safeS = `(?:(?!https?:\\/\\/)\\S)`; const domainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${hostsRegexPart})|(?|<\/p>/g, ''); return text.split('\n').map(line => { if (!line.trim()) return ''; return `>${line}`; }).join('\n'); }; renderer.paragraph = function (text) { return (typeof text === 'string') ? text : (text.text || ''); }; renderer.link = function (href, title, text) { if (typeof href === 'object' && href !== null) { title = href.title; text = href.text || text; href = href.href; } if (!href) return text || ''; const titleAttr = title ? ` title="${title}"` : ''; const isExternal = href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//'); let isSameSite = false; // Marked greedy autolink fix for spoiler brackets appended to URLs let extraSuffix = ''; const lowerHref = href.toLowerCase(); if (lowerHref.endsWith('%5b/spoiler%5d')) { href = href.substring(0, href.length - 14); text = text.replace(/\[\/spoiler\]/ig, ''); extraSuffix = '[/spoiler]'; } else if (lowerHref.endsWith('[/spoiler]')) { href = href.substring(0, href.length - 10); text = text.replace(/\[\/spoiler\]/ig, ''); extraSuffix = '[/spoiler]'; } if (href.startsWith(siteOrigin) || (href.startsWith('/') && !href.startsWith('//'))) { isSameSite = true; } else { try { const urlToParse = href.startsWith('//') ? window.location.protocol + href : href; const urlObj = new URL(urlToParse, siteOrigin); isSameSite = (urlObj.hostname === window.location.hostname); } catch (e) { } } let displayText = text; if (isSameSite && (text === href || text === href.replace(/^https?:\/\//, '') || text === href.replace(siteOrigin, ''))) { try { const urlToParse = href.startsWith('//') ? window.location.protocol + href : href; const url = new URL(urlToParse.startsWith('http') ? urlToParse : siteOrigin + (urlToParse.startsWith('/') ? '' : '/') + urlToParse); displayText = url.pathname + url.search + url.hash; } catch (e) { } } const isMention = href.startsWith('/user/') && text.startsWith('@'); if (isExternal && !isSameSite) { return `${displayText}${extraSuffix}`; } return `${displayText}${extraSuffix}`; }; renderer.image = function (href, title, text) { const src = (typeof href === 'object' && href !== null) ? (href.href || '') : (href || ''); const alt = text || ''; const ttl = title ? ` title="${title}"` : ''; return `${alt}`; }; // Line-by-line rendering to avoid paragraph collapsing and recursion const renderedLines = escaped.split('\n').map(line => { const trimmed = line.trimStart(); if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) { // Manual greentext handling — apply emoji if the user preference allows it const quoteContent = line.substring(line.indexOf('>') + 1); const quoteEmojis = window.f0ckSession?.quote_emojis === true; const rendered = quoteEmojis ? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n)) : quoteContent; return `>${rendered}`; } // Per-line limit to prevent marked.parse recursion on single giant lines if (line.length > 10000) return line; if (!line.trim()) return ' '; // Perform replacements on the single line let processedLine = line; // Handle Mentions processedLine = processedLine.replace(mentionRegex, (match, g1, g2) => { const user = g1 || g2; return `@${user}`; }); // Handle Comment Context Links (>>ID) processedLine = processedLine.replace(/(?>(\d+)/g, (match, id) => { const targetHref = itemId ? `/${itemId}#c${id}` : `#c${id}`; return `>>${id}`; }); processedLine = processedLine.replace(imageRegex, (match, url) => { let fullUrl = url; if (!url.startsWith('http') && !url.startsWith('//') && !url.startsWith('/')) { fullUrl = '//' + url; } return `![image](${fullUrl})`; }); processedLine = processedLine.replace(rawVideoRegex, (match, url) => { let fullUrl = url; if (!url.startsWith('http') && !url.startsWith('//') && !url.startsWith('/')) fullUrl = '//' + url; return `[video](${fullUrl})`; }); // Use marked for each line individually let mdSafe = processedLine.replace(/\*/g, '\\*').replace(/_/g, '\\_'); const bs = String.fromCharCode(92); mdSafe = mdSafe.split(bs + bs + '_').join(bs + bs + bs + '_'); let rendered = marked.parseInline ? marked.parseInline(mdSafe, { renderer: renderer }) : marked.parse(mdSafe, { renderer: renderer }).replace(/

|<\/p>/g, ''); // Render emojis ONLY if this is NOT a quote line OR if the user prefers it const quoteEmojis = window.f0ckSession?.quote_emojis === true; if (!trimmed.startsWith('>') || quoteEmojis) { rendered = rendered.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n)); } return rendered; }); let md = renderedLines.join('\n'); // YouTube label replacement: show icon + labeled link md = md.replace( /]*href="https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?(?:[^"]*&(?:amp;)?)?v=|youtu\.be\/)([a-zA-Z0-9_\-]{11})[^"]*"[^>]*>([\s\S]*?)<\/a>/gi, (match, videoId) => { const hrefMatch = match.match(/href="([^"]+)"/i); const ytHref = hrefMatch ? hrefMatch[1] : '#'; const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : ytHref); const externalAttr = (itemId && commentId) || commentId ? '' : ' target="_blank" rel="noopener noreferrer"'; return ` `; } ); // Vocaroo label replacement md = md.replace( /]*href="https?:\/\/(?:www\.)?(?:voca\.ro|vocaroo\.com)\/([a-zA-Z0-9_-]+)[^"]*"[^>]*>([\s\S]*?)<\/a>/gi, (match, vocarooId) => { if (['upload', 'contact', 'privacy', 'tos', 'about'].includes(vocarooId.toLowerCase())) return match; const hrefMatch = match.match(/href="([^"]+)"/i); const vocaHref = hrefMatch ? hrefMatch[1] : '#'; const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : vocaHref); const externalAttr = (itemId && commentId) || commentId ? '' : ' target="_blank" rel="noopener noreferrer"'; return ` Vocaroo Audio`; } ); // Build regex for allowed media hosters (video/audio) const escapedSiteHost = window.location.host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const mediaHosts = [escapedSiteHost]; if (window.f0ckAllowedImages && Array.isArray(window.f0ckAllowedImages)) { window.f0ckAllowedImages.forEach(h => { const escaped = h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); mediaHosts.push(`(?:[a-z0-9-]+\\.)*${escaped}`); }); } const mediaHostsPart = mediaHosts.join('|'); const mediaDomainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${mediaHostsPart})|(?=\\/[a-zA-Z0-9_\\-]))`; // Video label replacement: instead of embedding, show a link const videoEmbedRegex = new RegExp(`]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi'); md = md.replace(videoEmbedRegex, (match, url) => { const isConvertedGif = url.endsWith('#gif'); const cleanUrl = url.replace(/#gif$/, ''); // Converted GIFs → inline autoplay in sidebar too if (isConvertedGif) { return ``; } let isSameSite = false; try { const urlToParse = cleanUrl.startsWith('//') ? window.location.protocol + cleanUrl : cleanUrl; const urlObj = new URL(urlToParse, siteOrigin); isSameSite = (urlObj.hostname === window.location.hostname); } catch (e) { isSameSite = cleanUrl.startsWith(siteOrigin) || (cleanUrl.startsWith('/') && !cleanUrl.startsWith('//')); } const label = isSameSite ? 'Video Link' : 'External Video Link'; const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : cleanUrl); const externalAttr = (itemId && commentId) || commentId ? '' : ' target="_blank" rel="noopener noreferrer"'; return ` ${label} »`; }); // Handle spoilers [spoiler]text[/spoiler] (supports nesting) let prevMd; let iterations = 0; const spoilerRegex = /\[spoiler\]((?:(?!\[spoiler\])[\s\S])*?)\[\/spoiler\]/gi; do { prevMd = md; md = md.replace(spoilerRegex, (match, content) => { return `${content}`; }); iterations++; } while (md !== prevMd && iterations < 10); // Handle blur [blur]text[/blur] (supports nesting) const blurRegex = /\[blur\]((?:(?!\[blur\])[\s\S])*?)\[\/blur\]/gi; iterations = 0; do { prevMd = md; md = md.replace(blurRegex, (match, content) => { return `${content}`; }); iterations++; } while (md !== prevMd && iterations < 10); // Restore protected code blocks md = md.replace(/BLOCKPORTALX(\d+)X/g, (match, index) => { return codeBlocks[index] || ''; }); if (window.Sanitizer && typeof window.Sanitizer.clean === 'function') { md = window.Sanitizer.clean(md); } // Strip #gif from final text if it leaked through return md.replace(/#gif/g, ''); } catch (e) { return content; } }; const SIDEBAR_MAX_CHARS = 200; const SIDEBAR_MAX_EMOJIS = 12; const renderActivityItem = (c) => { const rawContent = c.content || c.body || ''; const displayContent = renderCommentContent(rawContent, c.id, c.item_id); // Build avatar URL — same priority as the rest of the app let avatarSrc = '/a/default.png'; if (c.avatar_file) { avatarSrc = `/a/${c.avatar_file}`; } else if (c.avatar) { avatarSrc = `/t/${c.avatar}.webp`; if (window.applyThumbCacheBust) avatarSrc = window.applyThumbCacheBust(avatarSrc); } const timeStr = c.created_at ? (window.f0ckTimeAgo ? window.f0ckTimeAgo(c.created_at) : (c.timeago || c.created_at)) : (c.timeago || 'just now'); const tsAttr = c.created_at ? ` data-ts="${escapeHtml(c.created_at)}"` : ''; let itemPreview = ''; if (c.item_id) { let mediaHtml = ''; let thumbUrl = `/t/${c.item_id}.webp`; if (window.applyThumbCacheBust) thumbUrl = window.applyThumbCacheBust(thumbUrl); mediaHtml = ``; itemPreview = `

${mediaHtml} ${(window.f0ckI18n && window.f0ckI18n.sidebar_view) || 'View'} »
`; } return `
${displayContent}
${itemPreview}
`; }; const checkOverflow = () => { document.querySelectorAll('.sidebar-activity .comment-content-inner').forEach(inner => { const container = inner.parentElement; const btn = container.querySelector('.read-more-btn'); if (!btn) return; // If expanded, always show "see less" if (container.classList.contains('expanded')) { btn.style.display = 'block'; btn.textContent = window.f0ckI18n?.sidebar_see_less || 'see less'; return; } if (inner.scrollHeight > inner.clientHeight + 2) { // 2px buffer for rounding btn.style.display = 'block'; btn.textContent = window.f0ckI18n?.sidebar_read_more || 'read more'; container.classList.add('has-overflow'); } else { btn.style.display = 'none'; container.classList.remove('has-overflow'); } }); }; const attachMediaLoadListeners = (element) => { element.querySelectorAll('img, video').forEach(media => { if (media.dataset.loadListenerBound) return; media.dataset.loadListenerBound = 'true'; media.addEventListener('load', checkOverflow, { once: true }); media.addEventListener('loadedmetadata', checkOverflow, { once: true }); }); }; // Event delegation — read-more expands, see-less collapses document.addEventListener('click', (e) => { // Read more / See less const readBtn = e.target.closest('.read-more-btn'); if (readBtn) { const contentDiv = readBtn.closest('.comment-content'); if (contentDiv) { contentDiv.classList.toggle('expanded'); checkOverflow(); // Re-sync button text and visibility } return; } }); const SIDEBAR_SKELETON_COUNT = 15; const showSkeletons = () => { const container = document.getElementById('sidebar-activity-container'); if (!container) return; const variants = [ `
`, `
`, `
`, `
`, `
`, ]; let html = ''; for (let i = 0; i < SIDEBAR_SKELETON_COUNT; i++) { html += ` `; } container.innerHTML = html; // Auto-play converted GIF videos container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => { }), { once: true }); }); }); }; const renderFromCache = () => { const container = document.getElementById('sidebar-activity-container'); if (!container || window._sidebarActivityCache.length === 0) return false; let html = ''; window._sidebarActivityCache.forEach(c => { html += renderActivityItem(c); }); container.innerHTML = html; // Re-append IO sentinel so the scroll observer keeps working after re-renders if (ioSentinel) { container.appendChild(ioSentinel); } attachMediaLoadListeners(container); checkOverflow(); fetchSidebarYoutubeTitles(container); // Auto-play converted GIF videos container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => { }), { once: true }); }); }); return true; }; const SIDEBAR_PAGE_LIMIT = 15; const SIDEBAR_INITIAL_LIMIT = 15; const loadActivity = async (silent = false) => { const container = document.getElementById('sidebar-activity-container'); 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) { showSkeletons(); } loading = true; currentPage = 1; hasMore = true; try { const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : ''; const res = await fetch(`/activity?json=true&page=1&limit=${SIDEBAR_INITIAL_LIMIT}&mode=${mode}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const data = await res.json(); if (data.success && data.comments && data.comments.length > 0) { window._sidebarActivityCache = data.comments.map(c => ({ ...c, body: c.content || c.body })); hasMore = data.hasMore === true || data.comments.length === SIDEBAR_INITIAL_LIMIT; renderFromCache(); // Also check after a delay to account for image/emoji loading shifts setTimeout(checkOverflow, 500); } else if (!hasCache) { container.innerHTML = '
No recent activity.
'; hasMore = false; } } catch (e) { console.error("Sidebar Activity: Failed to load activity", e); if (!hasCache) { container.innerHTML = '
Failed to load.
'; } hasMore = false; } finally { loading = false; } }; const loadMoreActivity = async () => { const container = document.getElementById('sidebar-activity-container'); if (!container || loading || loadingMore || !hasMore) return; loadingMore = true; // Use the current cache length as the exact offset so there's never a gap, // regardless of the initial fetch limit. const nextOffset = window._sidebarActivityCache.length; // Show a subtle loading row at the bottom const sentinel = document.createElement('div'); sentinel.id = 'sidebar-load-more-sentinel'; sentinel.style.cssText = 'text-align:center;padding:8px 0;font-size:0.78em;color:#666;'; sentinel.textContent = 'Loading…'; container.appendChild(sentinel); try { const mode = typeof window.activeMode !== 'undefined' ? window.activeMode : ''; const res = await fetch(`/activity?json=true&offset=${nextOffset}&limit=${SIDEBAR_PAGE_LIMIT}&mode=${mode}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const data = await res.json(); // Remove sentinel before inserting real content const s = document.getElementById('sidebar-load-more-sentinel'); if (s) s.remove(); if (data.success && data.comments && data.comments.length > 0) { currentPage++; hasMore = data.hasMore === true; // Append only comments not already in the cache const existingIds = new Set(window._sidebarActivityCache.map(c => String(c.id))); const newComments = data.comments.filter(c => !existingIds.has(String(c.id))).map(c => ({ ...c, body: c.content || c.body })); window._sidebarActivityCache.push(...newComments); // Append new items to DOM let html = ''; newComments.forEach(c => { html += renderActivityItem(c); }); if (html) { const temp = document.createElement('div'); temp.innerHTML = html; while (temp.firstElementChild) { container.appendChild(temp.firstElementChild); } // Keep the IO sentinel at the very end so it triggers on the next scroll if (ioSentinel) container.appendChild(ioSentinel); attachMediaLoadListeners(container); checkOverflow(); fetchSidebarYoutubeTitles(container); // Auto-play converted GIF videos container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => { }), { once: true }); }); }); } } else { hasMore = false; // Show end-of-feed indicator const end = document.createElement('div'); end.style.cssText = 'text-align:center;padding:8px 0;font-size:0.75em;color:#444;'; end.textContent = '─ end of activity ─'; container.appendChild(end); } } catch (e) { console.error("Sidebar Activity: Failed to load more", e); const s = document.getElementById('sidebar-load-more-sentinel'); if (s) s.remove(); } finally { loadingMore = false; } }; const handleNewActivity = (data) => { const container = document.getElementById('sidebar-activity-container'); // 1. Deduplicate: check if this comment ID is already in the cache if (window._sidebarActivityCache.some(c => parseInt(c.id) === parseInt(data.id))) { window.f0ckDebug("Sidebar Activity: Duplicate comment ignored", data.id); return; } // 2. Update cache (prepend, no hard cap — infinite scroll handles depth) const newItem = { ...data, body: data.body || data.content, timeago: (window.f0ckI18n && window.f0ckI18n.timeago_just_now) || 'just now' }; window._sidebarActivityCache.unshift(newItem); // Update DOM if visible if (container) { const html = renderActivityItem(newItem); const temp = document.createElement('div'); temp.innerHTML = html; const node = temp.firstElementChild; if (node) { node.classList.add('new-item-fade'); container.prepend(node); attachMediaLoadListeners(node); checkOverflow(); fetchSidebarYoutubeTitles(container); } } }; const init = async () => { await loadEmojis(); loadActivity(); }; // Listen for live activity from f0ckm.js document.addEventListener('f0ck:activityReceived', (e) => { window.f0ckDebug("Sidebar Activity: Live update received", e.detail); handleNewActivity(e.detail); }); const handleLiveEdit = (data) => { const container = document.getElementById('sidebar-activity-container'); // 1. Update cache if (window._sidebarActivityCache) { const comment = window._sidebarActivityCache.find(c => String(c.id) === String(data.comment_id)); if (comment) { comment.content = data.content; comment.body = data.content; } } // 2. Update DOM if visible if (container) { const el = document.getElementById('sc' + data.comment_id); if (el) { const inner = el.querySelector('.comment-content-inner'); if (inner) { const comment = window._sidebarActivityCache.find(c => String(c.id) === String(data.comment_id)); inner.innerHTML = renderCommentContent(data.content, data.comment_id, comment ? comment.item_id : null); el.classList.remove('new-item-fade'); void el.offsetWidth; el.classList.add('new-item-fade'); attachMediaLoadListeners(inner); checkOverflow(); fetchSidebarYoutubeTitles(el); // Auto-play converted GIF videos inner.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => { }), { once: true }); }); }); } } } }; window.addEventListener('f0ck:comment_edited', (e) => { window.f0ckDebug("Sidebar Activity: Live edit received", e.detail); handleLiveEdit(e.detail); }); let lastBoundMode = typeof window.activeMode !== 'undefined' ? window.activeMode : null; // Handle AJAX item loads document.addEventListener('f0ck:contentLoaded', () => { const currentMode = typeof window.activeMode !== 'undefined' ? window.activeMode : null; const modeChanged = lastBoundMode !== null && lastBoundMode !== currentMode; lastBoundMode = currentMode; window.f0ckDebug("Sidebar Activity: Page transition detected", modeChanged ? "(Mode changed)" : ""); if (modeChanged) { window._sidebarActivityCache = []; currentPage = 1; hasMore = true; loadActivity(false); // Force reload with loading state } else { // Immediately render from cache to avoid flicker renderFromCache(); // Background sync loadActivity(true); } }); // Sync sidebar and comments-list layout on initial page load (Legacy View Only) if (typeof syncSidebarAndComments === 'function') { syncSidebarAndComments(); } // Handle explicit mode changes (e.g. from item page where full transition doesn't occur) document.addEventListener('f0ck:modeChanged', (e) => { window.f0ckDebug("Sidebar Activity: Mode change detected", e.detail.mode); lastBoundMode = e.detail.mode; window._sidebarActivityCache = []; currentPage = 1; hasMore = true; loadActivity(false); }); // When the current user posts a comment, silently refresh sidebar to show it document.addEventListener('f0ck:commentPosted', () => { window.f0ckDebug("Sidebar Activity: Own comment posted, refreshing..."); loadActivity(true); }); // Infinite scroll: load older comments when scrolling near the bottom const bindScrollListener = () => { const container = document.getElementById('sidebar-activity-container'); if (!container) return; // Use IntersectionObserver if available (performant), fallback to scroll event if (typeof IntersectionObserver !== 'undefined') { // Create the sentinel once at module level so re-renders can re-append the same node if (!ioSentinel) { ioSentinel = document.createElement('div'); ioSentinel.id = 'sidebar-io-sentinel'; ioSentinel.style.height = '1px'; container.appendChild(ioSentinel); } const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore && !loadingMore && !loading) { loadMoreActivity(); } }, { root: container, rootMargin: '0px 0px 80px 0px', threshold: 0 }); observer.observe(ioSentinel); } else { container.addEventListener('scroll', () => { if (loading || loadingMore || !hasMore) return; const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 100; if (nearBottom) loadMoreActivity(); }, { passive: true }); } }; // Initial load const _origInit = init; const initWithScroll = async () => { await _origInit(); bindScrollListener(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initWithScroll); } else { initWithScroll(); } // Live updates are handled via SSE (f0ck:activityReceived event) })();