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.customEmojis = UserCommentSystem.emojiCache || {}; if (this.username) { this.init(); } } async init() { this.loadEmojis(); this.loadMore(); this.bindEvents(); } 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(); } }); } 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 res = await fetch('/user/' + encodeURIComponent(this.username) + '/comments?page=' + this.page + '&json=true'); const json = await res.json(); loader.remove(); if (json.success && json.comments.length > 0) { 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 = '
No comments found.
'; } } } catch (e) { console.error(e); loader.remove(); } finally { this.loading = false; } } renderEmoji(match, name) { if (this.customEmojis && this.customEmojis[name]) { return `${name}`; } return match; } renderCommentContent(content) { if (typeof marked === 'undefined') { console.error('UserCommentSystem: marked.js is undefined!'); return this.escapeHtml(content).replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n)); } try { // 1. Pre-process server-escaped content // Fix Greentext: Server sends >lool -> convert to > lool for marked let safe = content.replace(/^>/gm, '> '); // Also handle raw > just in case safe = safe.replace(/^>(?=[^ ])/gm, '> '); // Fix Images: Server sends -> convert to markdown ![]() or :emoji: // This avoids them being escaped by our HTML escaping later safe = safe.replace(/]*src="([^"]+)"[^>]*>/g, (match, src) => { let altMatch = match.match(/alt="([^"]*)"/); let alt = altMatch ? altMatch[1] : ''; // Check if it's a known emoji (convert back to :code: for consistent rendering) // We check if the name exists in our map. Validating src is good but name check is usually enough here. if (alt && this.customEmojis && this.customEmojis[alt]) { return `:${alt}:`; } return `![${alt}](${src})`; }); // 2. Escape HTML (standard safety) safe = safe .replace(/&/g, "&") .replace(/|<\/p>|\n/g, ''); return `> ${cleanQuote}
`; }; // 3. Parse Markdown let md = marked.parse(safe, { breaks: true, renderer: renderer }); return md.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n)); } catch (e) { console.error('UserCommentSystem Markdown Render Error:', e); return this.escapeHtml(content); } } renderComment(c) { const date = new Date(c.created_at).toLocaleString(); 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 `
${this.username} ${date} #${c.id} on Item #${c.item_id}
${content}
`; } escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(//g, ">"); } } window.addEventListener('DOMContentLoaded', () => { new UserCommentSystem(); });