(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 ``;
}
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 imageRegex = new RegExp(`(?|<\/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 `
`;
};
// 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 ``;
});
// Use marked for each line individually
const escapedAsterisks = processedLine.replace(/\*/g, '\\*');
let rendered = marked.parseInline ? marked.parseInline(escapedAsterisks, { renderer: renderer }) : marked.parse(escapedAsterisks, { 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\\[\\]\\(\\)]+)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
md = md.replace(videoEmbedRegex, (match, url) => {
let isSameSite = false;
try {
const urlToParse = url.startsWith('//') ? window.location.protocol + url : url;
const urlObj = new URL(urlToParse, siteOrigin);
isSameSite = (urlObj.hostname === window.location.hostname);
} catch(e) {
isSameSite = url.startsWith(siteOrigin) || (url.startsWith('/') && !url.startsWith('//'));
}
const label = isSameSite ? 'Video Link' : 'External Video Link';
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);
}
return md;
} 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 = `