if (!window.UserCommentSystem) { window.UserCommentSystem = class UserCommentSystem { constructor() { this.container = document.getElementById('user-comments-container'); this.username = this.container ? this.container.dataset.user : null; this.page = 1; this.loading = false; this.finished = false; this.userColor = null; this.customEmojis = UserCommentSystem.emojiCache || {}; this.icons = { reply: ``, link: `` }; if (this.username) { this.init(); } // Handle live updates for edited comments this.editListener = (e) => this.handleLiveEdit(e.detail); window.addEventListener('f0ck:comment_edited', this.editListener); } handleLiveEdit(data) { if (!this.container || !document.body.contains(this.container)) { window.removeEventListener('f0ck:comment_edited', this.editListener); return; } const el = document.getElementById('c' + data.comment_id); if (el && this.container.contains(el)) { const contentEl = el.querySelector('.comment-content'); if (contentEl) { contentEl.innerHTML = this.renderCommentContent(data.content, data.item_id); el.classList.remove('new-item-fade'); void el.offsetWidth; el.classList.add('new-item-fade'); } } } async init() { await this.loadEmojis(); this.loadMore(); this.loadMore(); this.bindEvents(); this.startLiveTimestamps(); } async loadEmojis() { if (UserCommentSystem.emojiCache) { this.customEmojis = UserCommentSystem.emojiCache; return; } try { const res = await fetch('/api/v2/emojis'); const data = await res.json(); if (data.success) { this.customEmojis = {}; data.emojis.forEach(e => { this.customEmojis[e.name] = e.url; }); UserCommentSystem.emojiCache = this.customEmojis; } } catch (e) { console.error("Failed to load emojis", e); } } bindEvents() { window.addEventListener('scroll', () => { if (this.loading || this.finished) return; if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 500) { this.loadMore(); } }); // Listen for mode changes document.addEventListener('f0ck:modeChange', (e) => { // Check if this instance is still active if (!document.body.contains(this.container)) return; window.f0ckDebug('Mode changed, reloading comments...'); this.container.innerHTML = ''; this.page = 1; this.finished = false; this.loadMore(); }); } async loadMore() { if (this.loading || this.finished) return; this.loading = true; const loader = document.createElement('div'); loader.className = 'loader-placeholder'; loader.innerText = 'Loading...'; loader.style.textAlign = 'center'; loader.style.padding = '10px'; this.container.appendChild(loader); try { const mode = window.activeMode || 'sfw'; const res = await fetch('/user/' + encodeURIComponent(this.username) + '/comments?page=' + this.page + '&json=true&mode=' + mode); const json = await res.json(); loader.remove(); if (json.success && json.comments.length > 0) { if (json.user && json.user.username_color) { this.userColor = json.user.username_color; } json.comments.forEach(c => { window.f0ckDebug('Raw Comment Content (ID ' + c.id + '):', c.content); const html = this.renderComment(c); this.container.insertAdjacentHTML('beforeend', html); }); this.page++; } else { this.finished = true; if (this.page === 1 && (!json.comments || json.comments.length === 0)) { this.container.innerHTML = '
${this.escapeHtml(content)}`;
}
if (typeof marked === 'undefined') {
return this.escapeHtml(content).replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n));
}
try {
// 1. Initial escaping using native method. Restore > for markdown markers.
let escaped = this.escapeHtml(content).replace(/>/g, ">");
// 2. Mentions
const mentionRegex = /(?|<\/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://');
const isSameSite = href.startsWith(siteOrigin);
let displayText = text;
if (isSameSite && (text === href || text === href.replace(/^https?:\/\//, '') || text === href.replace(siteOrigin, ''))) {
try {
const url = new URL(href.startsWith('http') ? href : siteOrigin + (href.startsWith('/') ? '' : '/') + href);
displayText = url.pathname + url.search + url.hash;
} catch (e) {}
}
if (isExternal && !isSameSite) {
return `${displayText}`;
}
return `${displayText}`;
};
renderer.image = (href, title, text) => {
const src = (typeof href === 'object' && href !== null) ? (href.href || '') : (href || '');
const imgHtml = `|<\/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) => this.renderEmoji(m, n)); } return rendered; }); let html = renderedLines.join('\n'); // Handle spoilers [spoiler]text[/spoiler] (supports nesting) let prevMd; let iterations = 0; const spoilerRegex = /\[spoiler\]((?:(?!\[spoiler\])[\s\S])*?)\[\/spoiler\]/gi; do { prevMd = html; html = html.replace(spoilerRegex, (match, content) => { return `${content}`; }); iterations++; } while (html !== prevMd && iterations < 10); // Handle blur [blur]text[/blur] (supports nesting) const blurRegex = /\[blur\]((?:(?!\[blur\])[\s\S])*?)\[\/blur\]/gi; iterations = 0; do { prevMd = html; html = html.replace(blurRegex, (match, content) => { return `${content}`; }); iterations++; } while (html !== prevMd && iterations < 10); if (window.Sanitizer && typeof Sanitizer.clean === 'function') { html = Sanitizer.clean(html); } return html; } catch (e) { console.error('UserCommentSystem Markdown Render Error:', e); return this.escapeHtml(content); } } renderComment(c) { const timeAgo = this.timeAgo(c.created_at); const fullDate = new Date(c.created_at).toISOString(); const content = this.renderCommentContent(c.content, c.item_id); return `