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 `
`;
}
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 ``;
});
// 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 `
`;
}
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(//g, ">");
}
}
window.addEventListener('DOMContentLoaded', () => {
new UserCommentSystem();
});