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) 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); el.classList.remove('new-item-fade'); void el.offsetWidth; el.classList.add('new-item-fade'); } } } async init() { 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; console.log('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 => { console.log('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}`;
};
// 3. Line-by-line rendering to avoid paragraph collapsing and recursion
const renderedLines = escaped.split('\n').map(line => {
const trimmed = line.trimStart();
if (trimmed.startsWith('>')) {
const quoteContent = line.substring(line.indexOf('>') + 1);
return `>${quoteContent}`;
}
// Per-line limit
if (line.length > 10000) return line;
if (!line.trim()) return ' ';
let processedLine = line.replace(mentionRegex, '[@$1](/user/$1)');
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) => 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); 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); // Replicating the structure of comments.js but adapting for the list view // We add a header indicating which item this comment belongs to return `