(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 renderCommentContent = (content) => {
if (!content) return '';
// Anti-recursion / Performance safeguard for extremely long comments
if (content.length > 50000) {
console.warn('Sidebar Activity: Comment too long, skipping markdown');
return `
${escapeHtml(content)}`;
}
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 `|<\/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) => {
const hrefMatch = match.match(/href="([^"]+)"/i);
const href = hrefMatch ? hrefMatch[1] : '#';
return ``;
}
);
// 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] || '';
});
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);
// 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`;
}
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 = '';
mediaHtml = `
`;
itemPreview = `