This commit is contained in:
2026-09-11 23:07:03 +02:00
parent 802a59d090
commit 4b615428de
14 changed files with 243 additions and 174 deletions
+17 -2
View File
@@ -2223,14 +2223,29 @@ class CommentSystem {
}
const bannerEnabled = window.f0ckCommentBannerEnabled !== false && window.f0ckSession?.comment_banner_enabled !== false;
const bannerStyle = (bannerEnabled && comment.banner_file && comment.banner_file !== 'null')
let bannerStyle = (bannerEnabled && comment.banner_file && comment.banner_file !== 'null')
? `style="--author-banner: url('/a/${comment.banner_file}'); --author-banner-position: ${comment.banner_position === 'center' ? 'center top' : (comment.banner_position || 'center top')}; --author-banner-size: ${(comment.banner_size && comment.banner_size !== 'cover') ? comment.banner_size : '100% auto'}; --author-banner-repeat: no-repeat;"`
: '';
const authorUserId = comment.user_id ?? (comment.username && window.f0ckSession && comment.username.toLowerCase() === (window.f0ckSession.user || '').toLowerCase() ? (window.f0ckSession.id || window.f0ckSession.user_id) : null);
const authorUsernameColor = comment.username_color || (comment.username && window.f0ckSession && comment.username.toLowerCase() === (window.f0ckSession.user || '').toLowerCase() ? window.f0ckSession.username_color : null);
return `<div class="${commentClass} ${isDeleted ? 'deleted' : ''} ${isPinned ? 'pinned' : ''}" id="c${comment.id}" ${bannerStyle}><div class="comment-avatar">${comment.username ? `<a href="/user/${comment.username}">` : ''}<img src="${comment.avatar_file ? `/a/${comment.avatar_file}` : (comment.avatar ? `/t/${comment.avatar}.webp` : '/a/default.png')}">${comment.username ? `</a>` : ''}</div><div class="comment-body"><div class="comment-header"><div class="comment-header-left">${pinnedBadge}${comment.username ? `<a href="/user/${comment.username}" class="comment-author" tooltip="ID: ${authorUserId ?? ''}" ${authorUsernameColor ? `style="color: ${authorUsernameColor}"` : ''}>${this.escapeHtml(comment.display_name || comment.username)}</a>` : '<span class="comment-author">System</span>'}${contextMarker}${backlinkHtml}</div><a href="#c${comment.id}" class="comment-time timeago" tooltip="${fullDate}" data-iso="${isoDate}" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}">${timeAgo}</a></div><div class="comment-content" data-raw="${this.escapeHtml(comment.content)}">${content}</div>${this.renderCommentAttachments(comment.files, comment.content)}${this.renderCommentPoll(comment.poll, comment.id, comment.username)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${!isDeleted && currentUserId ? `<button class="reply-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Reply"><i class="fa-solid fa-reply"></i></button><button class="quote-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Quote with Text"><i class="fa-solid fa-quote-left"></i></button><button class="report-comment-btn" data-id="${comment.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><i class="fa-solid fa-triangle-exclamation"></i></button>` : ''}${adminButtons}${userDeleteButton}</div></div></div></div><a href="#c${comment.id}" class="comment-permalink" title="Permalink" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}">#${comment.id}</a></div>${repliesHtml}`;
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
if (isAnonGuest) bannerStyle = '';
const avatarHtml = isAnonGuest
? `<div class="comment-avatar"><img src="/a/default.png"></div>`
: `<div class="comment-avatar">${comment.username ? `<a href="/user/${comment.username}">` : ''}<img src="${comment.avatar_file ? `/a/${comment.avatar_file}` : (comment.avatar ? `/t/${comment.avatar}.webp` : '/a/default.png')}">${comment.username ? `</a>` : ''}</div>`;
const authorHtml = isAnonGuest
? `<span class="comment-author">anonymous</span>`
: (comment.username
? `<a href="/user/${comment.username}" class="comment-author" tooltip="ID: ${authorUserId ?? ''}" ${authorUsernameColor ? `style="color: ${authorUsernameColor}"` : ''}>${this.escapeHtml(comment.display_name || comment.username)}</a>`
: '<span class="comment-author">System</span>');
const anonDataAttrs = isAnonGuest ? '' : `data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}"`;
return `<div class="${commentClass} ${isDeleted ? 'deleted' : ''} ${isPinned ? 'pinned' : ''}" id="c${comment.id}" ${bannerStyle}>${avatarHtml}<div class="comment-body"><div class="comment-header"><div class="comment-header-left">${pinnedBadge}${authorHtml}${contextMarker}${backlinkHtml}</div><a href="#c${comment.id}" class="comment-time timeago" tooltip="${fullDate}" data-iso="${isoDate}" data-id="${comment.id}" ${anonDataAttrs}>${timeAgo}</a></div><div class="comment-content" data-raw="${this.escapeHtml(comment.content)}">${content}</div>${this.renderCommentAttachments(comment.files, comment.content)}${this.renderCommentPoll(comment.poll, comment.id, isAnonGuest ? null : comment.username)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${!isDeleted && currentUserId ? `<button class="reply-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Reply"><i class="fa-solid fa-reply"></i></button><button class="quote-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Quote with Text"><i class="fa-solid fa-quote-left"></i></button><button class="report-comment-btn" data-id="${comment.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><i class="fa-solid fa-triangle-exclamation"></i></button>` : ''}${adminButtons}${userDeleteButton}</div></div></div></div><a href="#c${comment.id}" class="comment-permalink" title="Permalink" data-id="${comment.id}" ${anonDataAttrs}>#${comment.id}</a></div>${repliesHtml}`;
}
timeAgo(date) {
+103 -138
View File
@@ -6526,7 +6526,7 @@ window.cancelAnimFrame = (function () {
}
};
const initExcludedTagsModal = () => {
if (window.f0ckSession && !window.f0ckSession.logged_in) return;
const isLoggedIn = !!(window.f0ckSession && window.f0ckSession.logged_in);
const overlay = document.getElementById('excluded-tags-overlay');
if (!overlay) return;
@@ -6544,9 +6544,9 @@ window.cancelAnimFrame = (function () {
overlay.offsetHeight;
overlay.classList.add('visible');
document.body.style.overflow = 'hidden';
renderTags();
if (isLoggedIn) renderTags();
if (window.syncRatingButtonUI) window.syncRatingButtonUI();
if (window.innerWidth > 768) input.focus();
if (isLoggedIn && window.innerWidth > 768) input.focus();
} else {
overlay.classList.remove('visible');
document.body.style.overflow = '';
@@ -6611,149 +6611,105 @@ window.cancelAnimFrame = (function () {
if (e.target === overlay) toggleModal(false);
});
const addTag = async () => {
const tagname = input.value.trim();
if (!tagname) return;
suggestions.style.display = 'none';
try {
const res = await fetch('/api/v2/settings/excluded_tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tagname
})
});
const data = await res.json();
if (data.success) {
renderTags();
input.value = '';
} else {
window.flashMessage(data.msg || 'Error adding tag', 3000, 'error');
}
} catch (e) {
console.error(e);
} finally {
if (window.innerWidth > 768) input.focus();
}
};
input.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
// If a suggestion is highlighted, use it
if (highlightIdx >= 0) {
const items = suggestions.querySelectorAll('.tag-suggestion-item');
if (items[highlightIdx]) {
input.value = items[highlightIdx].querySelector('.tag-suggestion-name').textContent;
}
}
if (isLoggedIn) {
const addTag = async () => {
const tagname = input.value.trim();
if (!tagname) return;
suggestions.style.display = 'none';
addTag();
}
});
list.addEventListener('click', async e => {
if (e.target.classList.contains('remove-excluded-tag')) {
e.preventDefault();
const tag = e.target.getAttribute('data-tag');
try {
const res = await fetch(`/api/v2/settings/excluded_tags/${encodeURIComponent(tag)}`, {
method: 'DELETE'
const res = await fetch('/api/v2/settings/excluded_tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tagname })
});
const data = await res.json();
if (data.success) renderTags();
if (data.success) { renderTags(); input.value = ''; }
else window.flashMessage(data.msg || 'Error adding tag', 3000, 'error');
} catch (e) {
console.error(e);
} finally {
if (window.innerWidth > 768) input.focus();
}
}
});
// Autocomplete with dropdown suggestions
let debounceTimer = null;
let highlightIdx = -1;
const renderSuggestions = (items) => {
suggestions.innerHTML = '';
highlightIdx = -1;
if (!items.length) {
suggestions.style.display = 'none';
return;
}
items.forEach(s => {
const div = document.createElement('div');
div.className = 'tag-suggestion-item';
const name = document.createElement('span');
name.className = 'tag-suggestion-name';
name.textContent = s.tag;
const meta = document.createElement('span');
meta.className = 'tag-suggestion-meta';
meta.textContent = `${s.tagged}× · ${s.score.toFixed(2)}`;
div.appendChild(name);
div.appendChild(meta);
div.addEventListener('mousedown', (e) => {
};
input.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
input.value = s.tag;
if (highlightIdx >= 0) {
const items = suggestions.querySelectorAll('.tag-suggestion-item');
if (items[highlightIdx]) input.value = items[highlightIdx].querySelector('.tag-suggestion-name').textContent;
}
suggestions.style.display = 'none';
addTag();
});
suggestions.appendChild(div);
});
suggestions.style.display = 'block';
};
input.addEventListener('input', () => {
const q = input.value.trim();
highlightIdx = -1;
if (q.length < 1) {
suggestions.style.display = 'none';
return;
}
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
try {
const res = await fetch(`/api/v2/tags/suggest?q=${encodeURIComponent(q)}`);
const json = await res.json();
if (json.success && json.suggestions) {
renderSuggestions(json.suggestions.slice(0, 8));
} else {
suggestions.style.display = 'none';
}
} catch (e) {
suggestions.style.display = 'none';
}
}, 300);
});
});
input.addEventListener('keydown', e => {
const items = suggestions.querySelectorAll('.tag-suggestion-item');
if (!items.length || suggestions.style.display === 'none') return;
list.addEventListener('click', async e => {
if (e.target.classList.contains('remove-excluded-tag')) {
e.preventDefault();
const tag = e.target.getAttribute('data-tag');
try {
const res = await fetch(`/api/v2/settings/excluded_tags/${encodeURIComponent(tag)}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) renderTags();
} catch (e) { console.error(e); }
}
});
if (e.key === 'ArrowDown') {
e.preventDefault();
if (highlightIdx >= 0 && highlightIdx < items.length) items[highlightIdx].classList.remove('active');
highlightIdx = highlightIdx < items.length - 1 ? highlightIdx + 1 : 0;
items[highlightIdx].classList.add('active');
items[highlightIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (highlightIdx >= 0 && highlightIdx < items.length) items[highlightIdx].classList.remove('active');
highlightIdx = highlightIdx > 0 ? highlightIdx - 1 : items.length - 1;
items[highlightIdx].classList.add('active');
items[highlightIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Escape') {
suggestions.style.display = 'none';
// Autocomplete with dropdown suggestions
let debounceTimer = null;
let highlightIdx = -1;
const renderSuggestions = (items) => {
suggestions.innerHTML = '';
highlightIdx = -1;
}
});
if (!items.length) { suggestions.style.display = 'none'; return; }
items.forEach(s => {
const div = document.createElement('div');
div.className = 'tag-suggestion-item';
const name = document.createElement('span'); name.className = 'tag-suggestion-name'; name.textContent = s.tag;
const meta = document.createElement('span'); meta.className = 'tag-suggestion-meta'; meta.textContent = `${s.tagged}× · ${s.score.toFixed(2)}`;
div.appendChild(name); div.appendChild(meta);
div.addEventListener('mousedown', (e) => { e.preventDefault(); input.value = s.tag; suggestions.style.display = 'none'; addTag(); });
suggestions.appendChild(div);
});
suggestions.style.display = 'block';
};
input.addEventListener('blur', () => {
setTimeout(() => { suggestions.style.display = 'none'; }, 150);
});
input.addEventListener('input', () => {
const q = input.value.trim();
highlightIdx = -1;
if (q.length < 1) { suggestions.style.display = 'none'; return; }
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
try {
const res = await fetch(`/api/v2/tags/suggest?q=${encodeURIComponent(q)}`);
const json = await res.json();
if (json.success && json.suggestions) renderSuggestions(json.suggestions.slice(0, 8));
else suggestions.style.display = 'none';
} catch (e) { suggestions.style.display = 'none'; }
}, 300);
});
input.addEventListener('keydown', e => {
const items = suggestions.querySelectorAll('.tag-suggestion-item');
if (!items.length || suggestions.style.display === 'none') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
if (highlightIdx >= 0 && highlightIdx < items.length) items[highlightIdx].classList.remove('active');
highlightIdx = highlightIdx < items.length - 1 ? highlightIdx + 1 : 0;
items[highlightIdx].classList.add('active'); items[highlightIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (highlightIdx >= 0 && highlightIdx < items.length) items[highlightIdx].classList.remove('active');
highlightIdx = highlightIdx > 0 ? highlightIdx - 1 : items.length - 1;
items[highlightIdx].classList.add('active'); items[highlightIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Escape') {
suggestions.style.display = 'none'; highlightIdx = -1;
}
});
input.addEventListener('blur', () => { setTimeout(() => { suggestions.style.display = 'none'; }, 150); });
} // end isLoggedIn tag block
// ESC to close, "e" to open
document.addEventListener('keydown', (e) => {
@@ -6922,30 +6878,39 @@ window.cancelAnimFrame = (function () {
const THUMB_SIZES = ['s', 'm', 'l', 'xl'];
const gridSizeBtns = overlay.querySelectorAll('.nav-grid-size-btn');
const applyNavThumbSize = (size) => {
if (!THUMB_SIZES.includes(size)) size = 'm';
const applyNavThumbSize = (size, persist = true) => {
if (!THUMB_SIZES.includes(size)) size = window.f0ckDefaultThumbSize || 'm';
document.documentElement.dataset.thumbSize = size;
localStorage.setItem(THUMB_SIZE_KEY, size);
if (persist) localStorage.setItem(THUMB_SIZE_KEY, size);
gridSizeBtns.forEach(b => b.classList.toggle('active', b.dataset.thumbSize === size));
};
// Sync active state each time the modal opens
if (filterBtn) {
filterBtn.addEventListener('click', () => {
const cur = localStorage.getItem(THUMB_SIZE_KEY) || 'm';
const cur = localStorage.getItem(THUMB_SIZE_KEY) || window.f0ckDefaultThumbSize || 'm';
gridSizeBtns.forEach(b => b.classList.toggle('active', b.dataset.thumbSize === cur));
});
}
gridSizeBtns.forEach(btn => {
btn.addEventListener('click', () => applyNavThumbSize(btn.dataset.thumbSize));
btn.addEventListener('click', () => applyNavThumbSize(btn.dataset.thumbSize, true));
});
// Apply persisted size immediately on init (every page load)
applyNavThumbSize(localStorage.getItem(THUMB_SIZE_KEY) || 'm');
// Apply on init — do NOT persist so config default stays effective until user chooses
applyNavThumbSize(localStorage.getItem(THUMB_SIZE_KEY) || window.f0ckDefaultThumbSize || 'm', false);
};
// ── Thumb size init (runs on every page, independently of the filter modal) ──
const initThumbSize = () => {
const THUMB_SIZE_KEY = 'scroller_thumb_size';
const THUMB_SIZES = ['s', 'm', 'l', 'xl'];
const size = localStorage.getItem(THUMB_SIZE_KEY) || window.f0ckDefaultThumbSize || 'm';
document.documentElement.dataset.thumbSize = THUMB_SIZES.includes(size) ? size : (window.f0ckDefaultThumbSize || 'm');
};
document.addEventListener('DOMContentLoaded', () => {
initThumbSize(); // always runs — applies data-thumb-size to <html>
initSearch();
initExcludedTagsModal();
if (window.updateFilterBadge) window.updateFilterBadge();
+28 -17
View File
@@ -100,27 +100,27 @@
// ── Grid / Thumbnail size ─────────────────────────────────────────────────
const THUMB_SIZES = ['s', 'm', 'l', 'xl'];
let thumbSize = localStorage.getItem(THUMB_SIZE_KEY) || 'm';
if (!THUMB_SIZES.includes(thumbSize)) thumbSize = 'm';
let thumbSize = localStorage.getItem(THUMB_SIZE_KEY) || window.f0ckDefaultThumbSize || 'm';
if (!THUMB_SIZES.includes(thumbSize)) thumbSize = window.f0ckDefaultThumbSize || 'm';
function applyThumbSize(size) {
function applyThumbSize(size, persist = true) {
thumbSize = size;
document.documentElement.dataset.thumbSize = size;
localStorage.setItem(THUMB_SIZE_KEY, size);
if (persist) localStorage.setItem(THUMB_SIZE_KEY, size);
// Sync pill active state whenever called outside the click handler
document.querySelectorAll('#thumb-size-pills .filter-pill').forEach(p =>
p.classList.toggle('active', p.dataset.thumbSize === size)
);
}
// Apply persisted size on load
applyThumbSize(thumbSize);
// Apply on init — do NOT persist so config default stays effective until user chooses
applyThumbSize(thumbSize, false);
// Wire pill click events
const thumbSizePillsEl = document.getElementById('thumb-size-pills');
if (thumbSizePillsEl) {
thumbSizePillsEl.querySelectorAll('.filter-pill').forEach(pill => {
pill.addEventListener('click', () => {
applyThumbSize(pill.dataset.thumbSize);
applyThumbSize(pill.dataset.thumbSize, true);
});
});
}
@@ -1373,7 +1373,8 @@
const meta = document.createElement('div'); meta.className = 'scroll-meta';
// esc() the color value: a raw '"' in username_color would break out of the
// style attribute and allow arbitrary HTML injection (XSS).
const colorStyle = item.username_color ? `color:${esc(item.username_color)}` : '';
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const colorStyle = (!isAnonGuest && item.username_color) ? `color:${esc(item.username_color)}` : '';
const ratingHtml = `<span class="scroll-rating ${esc(item.rating_class)}" data-item-id="${item.id}" data-rating="${esc(item.rating_class)}">${esc(item.rating_label)}</span>`;
const ocHtml = item.is_oc ? `<span class="scroll-oc"><i class="fa-solid fa-star" style="font-size:.6rem"></i> OC</span>` : '';
const badgesHtml = `<div class="scroll-badges">${ratingHtml}${ocHtml}</div>`;
@@ -1383,10 +1384,15 @@
item.tags.split(', ').map(t => `<span class="scroll-tag-pill" data-tag="${esc(t.trim())}">${esc(t.trim())}</span>`).join('')
}</div>`
: '';
meta.innerHTML = `
<div class="scroll-meta-inner">
${badgesHtml}
<div class="scroll-meta-top">
const userMetaHtml = isAnonGuest
? `<div class="scroll-meta-top">
<img class="scroll-avatar" src="/a/default.png" alt="" loading="lazy">
<div>
<div class="scroll-username">anonymous</div>
<div class="scroll-timeago">${esc(item.stamp ? timeAgo(item.stamp * 1000) : (item.timeago || ''))}</div>
</div>
</div>`
: `<div class="scroll-meta-top">
<a href="/user/${esc(item.username)}" class="scroll-user-link">
<img class="scroll-avatar" src="${esc(item.avatar)}" alt="" loading="lazy" onerror="this.src='/a/default.png'">
</a>
@@ -1396,7 +1402,11 @@
</a>
<div class="scroll-timeago">${esc(item.stamp ? timeAgo(item.stamp * 1000) : (item.timeago || ''))}</div>
</div>
</div>
</div>`;
meta.innerHTML = `
<div class="scroll-meta-inner">
${badgesHtml}
${userMetaHtml}
${tagsHtml}
${item.is_external && item.external_board && item.external_tid
? `<a class="scroll-id-link" href="https://boards.4chan.org/${item.external_board}/thread/${item.external_tid}#p${item.external_id}" target="_blank">/${item.external_board}/thread/${item.external_tid}</a>`
@@ -2187,10 +2197,11 @@
function renderCommentEl(c, canReply) {
const el = document.createElement('div'); el.className = 'comment-item';
el.dataset.commentId = c.id || '';
const av = c.avatar_file ? `/a/${c.avatar_file}` : (c.avatar ? `/t/${c.avatar}.webp` : '/a/default.png');
const nc = c.username_color ? `color:${esc(c.username_color)}` : '';
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const av = isAnonGuest ? '/a/default.png' : (c.avatar_file ? `/a/${c.avatar_file}` : (c.avatar ? `/t/${c.avatar}.webp` : '/a/default.png'));
const nc = (!isAnonGuest && c.username_color) ? `color:${esc(c.username_color)}` : '';
const _i = window.f0ckI18n || {};
const uname = c.display_name || c.username || 'anon';
const uname = isAnonGuest ? 'anonymous' : (c.display_name || c.username || 'anon');
el.innerHTML = `
<img class="comment-avatar" src="${esc(av)}" alt="" loading="lazy" onerror="this.src='/a/default.png'">
<div class="comment-body">
@@ -2198,7 +2209,7 @@
<div class="comment-content" data-raw="${esc(c.content || '')}">${renderCommentContent(c.content || '')}</div>
<div class="comment-meta">
<span class="comment-time">${c.created_at ? timeAgo(c.created_at) : (_i.ta_just_now || _i.just_now || 'just now')}</span>
${canReply ? `
${(!isAnonGuest && canReply) ? `
<button class="comment-reply-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.reply || 'Reply'}</button>
<button class="comment-quote-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.quote || 'Quote'}</button>
` : ''}
+12 -5
View File
@@ -562,15 +562,22 @@
? `style="--author-banner: url('/a/${bannerFile}'); --author-banner-position: ${bannerPos === 'center' ? 'center top' : (bannerPos || 'center top')}; --author-banner-size: ${(bannerSz && bannerSz !== 'cover') ? bannerSz : '100% auto'}; --author-banner-repeat: no-repeat;"`
: '';
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const effectiveBannerStyle = isAnonGuest ? '' : bannerStyle;
const authorAvatarHtml = isAnonGuest
? `<span class="sidebar-avatar-link"><img src="/a/default.png" class="sidebar-avatar" loading="eager" /></span>`
: `<a href="/user/${c.username.toLowerCase()}" class="sidebar-avatar-link"><img src="${avatarSrc}" class="sidebar-avatar" loading="eager" onload="this.classList.add('loaded')" onerror="this.classList.add('loaded');this.src='/a/default.png'" /></a>`;
const authorNameHtml = isAnonGuest
? `<span class="comment-author">anonymous</span>`
: `<a href="/user/${c.username.toLowerCase()}" class="comment-author" ${c.username_color ? `style="color: ${c.username_color}"` : ''}>${escapeHtml(c.display_name || c.username)}</a>`;
return `
<div class="comment" id="sc${c.id}" ${bannerStyle}>
<div class="comment" id="sc${c.id}" ${effectiveBannerStyle}>
<div class="comment-body">
<div class="comment-header">
<div class="comment-header-left">
<a href="/user/${c.username.toLowerCase()}" class="sidebar-avatar-link">
<img src="${avatarSrc}" class="sidebar-avatar" loading="eager" onload="this.classList.add('loaded')" onerror="this.classList.add('loaded');this.src='/a/default.png'" />
</a>
<a href="/user/${c.username.toLowerCase()}" class="comment-author" ${c.username_color ? `style="color: ${c.username_color}"` : ''}>${escapeHtml(c.display_name || c.username)}</a>
${authorAvatarHtml}
${authorNameHtml}
</div>
<span class="comment-time timeago" tooltip="${fullDate}" style="font-size: 0.75em;"${tsAttr}>${timeStr}</span>
</div>