This commit is contained in:
2026-08-07 21:42:12 +02:00
parent c7414e4b21
commit feb408338a
40 changed files with 927 additions and 229 deletions

View File

@@ -1,18 +1,24 @@
(async () => {
// Helper to get dynamic context
const getContext = () => {
const idLink = document.querySelector("a.id-link");
if (!idLink) return null;
const commentsEl = document.querySelector("#comments-container");
const favoEl = document.querySelector("#a_favo");
const infoEl = document.querySelector("#a_info");
const idLinkEl = document.querySelector("a.id-link");
const rawId = commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText;
if (!rawId) return null;
const tagsContainer = document.querySelector("#tags");
const inner = tagsContainer.querySelector(".tags-inner") || tagsContainer;
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
const usernameEl = document.querySelector("a#a_username");
return {
postid: +idLink.innerText,
postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(),
// data-username holds the raw DB username; data-author-id holds the user's numeric ID.
// Never fall back to innerText — it may be a display name or the literal string 'unknown'.
poster: (usernameEl?.dataset?.username || '').trim() || null,
authorId: (usernameEl?.dataset?.authorId || '').trim() || null,
tags: [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2))
tags: inner ? [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2)) : []
};
};

View File

@@ -2405,10 +2405,50 @@ class CommentSystem {
}
}, { passive: true });
// Global click listener to close popups (useful for mobile dismissal)
// Global click listener for comment interaction (popups & expanding truncated comments in previews)
document.addEventListener('click', (e) => {
const isLink = e.target.closest('.comment-context-link');
const isPopup = e.target.closest('.comment-preview-popup');
const target = e.target;
// Load full comment (expand truncated)
const loadFullBtn = target.closest('.load-full-comment-btn');
if (loadFullBtn) {
const contentEl = loadFullBtn.closest('.comment-content');
if (contentEl) {
if (contentEl.querySelector('.collapse-comment-btn')) return;
const commentEl = contentEl.closest('.comment');
const commentId = commentEl ? (commentEl.dataset.id || (commentEl.id ? commentEl.id.replace(/^c/, '') : null)) : null;
const fullContent = contentEl.dataset.raw || (commentId && this.commentCache ? this.commentCache.get(commentId)?.content : null);
if (fullContent) {
contentEl.innerHTML = this.renderCommentContent(fullContent, null, true);
const seeLessLabel = (window.f0ckI18n?.sidebar_see_less) || 'see less';
contentEl.insertAdjacentHTML('beforeend',
`<span class="item-comment-truncated-notice"><button class="collapse-comment-btn" type="button">${seeLessLabel}</button></span>`
);
CommentSystem.playEmojiVideos(contentEl);
}
}
return;
}
// Collapse full comment back to truncated view
const collapseBtn = target.closest('.collapse-comment-btn');
if (collapseBtn) {
const contentEl = collapseBtn.closest('.comment-content');
if (contentEl) {
if (contentEl.querySelector('.load-full-comment-btn')) return;
const commentEl = contentEl.closest('.comment');
const commentId = commentEl ? (commentEl.dataset.id || (commentEl.id ? commentEl.id.replace(/^c/, '') : null)) : null;
const fullContent = contentEl.dataset.raw || (commentId && this.commentCache ? this.commentCache.get(commentId)?.content : null);
if (fullContent) {
contentEl.innerHTML = this.renderCommentContent(fullContent, null, false);
CommentSystem.playEmojiVideos(contentEl);
}
}
return;
}
const isLink = target.closest('.comment-context-link');
const isPopup = target.closest('.comment-preview-popup');
if (!isLink && !isPopup) {
this.closePreviewsAboveLevel(-1);
@@ -2823,7 +2863,10 @@ class CommentSystem {
if (loadFullBtn) {
const contentEl = loadFullBtn.closest('.comment-content');
if (contentEl) {
const fullContent = contentEl.dataset.raw;
if (contentEl.querySelector('.collapse-comment-btn')) return;
const commentEl = contentEl.closest('.comment');
const commentId = commentEl ? (commentEl.dataset.id || (commentEl.id ? commentEl.id.replace(/^c/, '') : null)) : null;
const fullContent = contentEl.dataset.raw || (commentId && this.commentCache ? this.commentCache.get(commentId)?.content : null);
if (fullContent) {
contentEl.innerHTML = this.renderCommentContent(fullContent, null, true);
// Append "see less" button after full content
@@ -2831,6 +2874,7 @@ class CommentSystem {
contentEl.insertAdjacentHTML('beforeend',
`<span class="item-comment-truncated-notice"><button class="collapse-comment-btn" type="button">${seeLessLabel}</button></span>`
);
CommentSystem.playEmojiVideos(contentEl);
}
}
return;
@@ -2841,9 +2885,13 @@ class CommentSystem {
if (collapseBtn) {
const contentEl = collapseBtn.closest('.comment-content');
if (contentEl) {
const fullContent = contentEl.dataset.raw;
if (contentEl.querySelector('.load-full-comment-btn')) return;
const commentEl = contentEl.closest('.comment');
const commentId = commentEl ? (commentEl.dataset.id || (commentEl.id ? commentEl.id.replace(/^c/, '') : null)) : null;
const fullContent = contentEl.dataset.raw || (commentId && this.commentCache ? this.commentCache.get(commentId)?.content : null);
if (fullContent) {
contentEl.innerHTML = this.renderCommentContent(fullContent, null, false);
CommentSystem.playEmojiVideos(contentEl);
}
}
return;

View File

@@ -965,7 +965,7 @@ window.cancelAnimFrame = (function () {
'login-modal', 'register-modal', 'forgot-modal', 'reset-modal',
'report-modal', 'halls-modal', 'metadata-modal', 'warning-modal',
'shortcuts-modal', 'upload-drag-modal', 'excluded-tags-overlay',
'content-warning-modal', 'gchat-img-modal', 'image-modal', 'info-modal'
'content-warning-modal', 'gchat-img-modal', 'image-modal', 'info-modal', 'visibility-modal'
];
modalIds.forEach(id => {
// Don't close the filter modal during a background mime-filter reload
@@ -2442,8 +2442,8 @@ window.cancelAnimFrame = (function () {
const isUpload = pathname.match(/\/upload\/?(?:$|\?)/);
const parts = pathname.split('/').filter(Boolean);
const isItem = !pathname.match(/\/p\//) && (
pathname.match(/^\/\d+/) ||
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && /^\d+$/.test(parts[parts.length - 1]))
pathname.match(/^\/\d+/) || pathname.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) ||
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1])))
);
const isMessages = !!pathname.match(/^\/messages(\/|$)/);
const isAbyss = !!pathname.match(/^\/abyss(\/|$|\?|#)/) || pathname === '/abyss';
@@ -3356,19 +3356,19 @@ window.cancelAnimFrame = (function () {
// Extract item ID from URL. Use the last numeric segment to avoid matching context IDs (like tag/1/...)
// Split path, filter numeric, pop last.
const pathSegments = new URL(url, window.location.origin).pathname.split('/');
const numericSegments = pathSegments.filter(s => /^\d+$/.test(s));
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
// Clear and Hide navbar pagination for Item View (Never show grid pagination on item view)
// document.querySelectorAll('.pagination-wrapper').forEach(el => el.innerHTML = ''); // Don't destroy content (cached grid needs it)
document.querySelectorAll('.pagination-container-fluid').forEach(el => el.style.display = 'none');
if (numericSegments.length === 0) {
console.warn("loadItemAjax: No ID match found in URL", url);
if (keySegments.length === 0) {
console.warn("loadItemAjax: No ID or slug match found in URL", url);
// fallback for weird/external links
window.location.href = url;
return;
}
const itemid = numericSegments.pop();
const itemid = keySegments.pop();
// Extract context from Target URL first
let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null, tagger = null;
@@ -3551,18 +3551,19 @@ window.cancelAnimFrame = (function () {
}
const _hash = new URL(url, window.location.origin).hash;
let _pushUrl = `/${itemid}`;
if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemid}`;
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemid}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemid}`; }
else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemid}`;
else if (hall) _pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g,' ')}/${itemid}`;
const itemKey = _cachedItem.slug || itemid;
let _pushUrl = `/${itemKey}`;
if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemKey}`; }
else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemKey}`;
else if (hall) _pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g,' ')}/${itemKey}`;
if (tagger && tag) _pushUrl += `?tagger=${encodeURIComponent(tagger)}`;
if (mime) _pushUrl = _pushUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`);
if (mime) _pushUrl = _pushUrl.replace(new RegExp(`/${itemKey}$`), `/${mime}/${itemKey}`);
if (_hash) _pushUrl += _hash;
if (!options.keepMedia && !options.skipPush) history.pushState({}, '', _pushUrl);
document.title = `${window.f0ckDomain} - ${itemid}`;
document.title = `${window.f0ckDomain} - ${itemKey}`;
if (navbar) navbar.classList.remove('pbwork');
if (!options.keepMedia) {
@@ -3587,14 +3588,14 @@ window.cancelAnimFrame = (function () {
.then(r => r.ok ? r.text() : null)
.then(freshText => {
if (!freshText) return;
try { JSON.parse(freshText); } catch(_) {}
// Re-parse to get html string
let freshHtml = freshText;
let freshSlug = null;
try {
const d = JSON.parse(freshText);
if (d && typeof d.html === 'string') freshHtml = d.html;
if (d && (d.slug || d.item?.slug)) freshSlug = d.slug || d.item?.slug;
} catch(_) {}
itemCacheMap.set(_itemCacheKey, { html: freshHtml, ts: Date.now() });
itemCacheMap.set(_itemCacheKey, { html: freshHtml, slug: freshSlug, ts: Date.now() });
window.f0ckDebug('[itemCache] Background revalidation complete for', _itemCacheKey);
})
.catch(() => {});
@@ -3619,7 +3620,7 @@ window.cancelAnimFrame = (function () {
- Total Network: ${(tBody - tStart).toFixed(2)}ms
- Content Size: ${(rawText.length / 1024).toFixed(2)} KB`);
let html, paginationHtml;
let html, paginationHtml, responseSlug = null;
try {
// Optimistically try to parse as JSON first
@@ -3632,6 +3633,7 @@ window.cancelAnimFrame = (function () {
if (data && typeof data.html === 'string') {
html = data.html;
paginationHtml = data.pagination;
responseSlug = data.slug || data.item?.slug || null;
} else {
html = rawText;
}
@@ -3642,7 +3644,7 @@ window.cancelAnimFrame = (function () {
// ── Store in item cache (stale-while-revalidate) ───────────────────────
if (html && !options.skipCache) {
itemCacheMap.set(_itemCacheKey, { html, ts: Date.now() });
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, ts: Date.now() });
if (itemCacheMap.size > ITEM_CACHE_MAX) {
// Evict oldest entry
itemCacheMap.delete(itemCacheMap.keys().next().value);
@@ -3749,26 +3751,25 @@ window.cancelAnimFrame = (function () {
// Construct proper History URL (Context Aware)
// If we inherited context, we should reflect it in the URL
const hash = new URL(url, window.location.origin).hash;
let pushUrl = `/${itemid}`;
const itemKey = responseSlug || itemid;
let pushUrl = `/${itemKey}`;
// Logic from ajax.mjs context reconstruction:
if (userHall && userHallOwner) {
pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemid}`;
pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
} else if (user) {
pushUrl = `/user/${encodeURIComponent(user)}/${itemid}`;
if (isFavs) pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemid}`;
pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`;
if (isFavs) pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemKey}`;
}
else if (tag) pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${itemid}`;
else if (hall) pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g, ' ')}/${itemid}`;
else if (tag) pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${itemKey}`;
else if (hall) pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g, ' ')}/${itemKey}`;
// Append tagger filter so item nav stays in tagger context
if (tagger && tag) pushUrl += `?tagger=${encodeURIComponent(tagger)}`;
if (mime) {
// If it already has itemid at the end, insert mime before it
pushUrl = pushUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`);
// If it already has itemKey at the end, insert mime before it
pushUrl = pushUrl.replace(new RegExp(`/${itemKey}$`), `/${mime}/${itemKey}`);
}
// Re-append hash if present
if (hash) pushUrl += hash;
@@ -3788,8 +3789,7 @@ window.cancelAnimFrame = (function () {
if (window.initVisualizer) window.initVisualizer();
}
// Try to extract ID from response if possible or just use itemid
document.title = `${window.f0ckDomain} - ${itemid}`;
if (navbar) navbar.classList.remove("pbwork");
document.title = `${window.f0ckDomain} - ${itemKey}`;
window.f0ckDebug("AJAX load complete");
// Notify extensions — also triggers CommentSystem init which renders comments
@@ -3821,6 +3821,7 @@ window.cancelAnimFrame = (function () {
console.error("AJAX load failed:", err);
} finally {
isNavigating = false;
if (navbar) navbar.classList.remove("pbwork");
}
};
@@ -3999,15 +4000,16 @@ window.cancelAnimFrame = (function () {
fetch(randomUrl)
.then(r => r.json())
.then(data => {
if (data.success && data.items && data.items.id) {
if (data.success && data.items && (data.items.slug || data.items.id)) {
const targetKey = data.items.slug || data.items.id;
// Navigate in the same context (user hall, favs, tag, etc.)
if (wUserHall && wUserHallOwner) {
loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${data.items.id}`, true);
loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${targetKey}`, true);
} else if (wFavsUser) {
// Preserve /user/:name/favs/:id context so next/prev arrows stay within favs
loadItemAjax(`/user/${encodeURIComponent(wFavsUser)}/favs/${data.items.id}`, true);
loadItemAjax(`/user/${encodeURIComponent(wFavsUser)}/favs/${targetKey}`, true);
} else {
loadItemAjax(`/${data.items.id}`, true);
loadItemAjax(`/${targetKey}`, true);
}
} else {
window.location.href = link.href;
@@ -4079,7 +4081,7 @@ window.cancelAnimFrame = (function () {
return false;
}
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes)/) || pathname.startsWith('/abyss'))) {
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes)/) || pathname.startsWith('/abyss'))) {
e.preventDefault();
e.stopImmediatePropagation();
@@ -4092,9 +4094,10 @@ window.cancelAnimFrame = (function () {
const parts = pathname.split('/').filter(Boolean);
const isItemLink = !pathname.match(/\/p\//) && (
pathname.match(/^\/\d+/) ||
(parts.length >= 3 && parts[0] === 'tag' && /^\d+$/.test(parts[parts.length - 1])) ||
(parts.length >= 3 && parts[0] === 'user' && /^\d+$/.test(parts[parts.length - 1])) ||
(parts.length >= 3 && parts[0] === 'h' && /^\d+$/.test(parts[parts.length - 1]))
pathname.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) ||
(parts.length >= 3 && parts[0] === 'tag' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
(parts.length >= 3 && parts[0] === 'user' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
(parts.length >= 3 && parts[0] === 'h' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1])))
);
if (isItemLink) {
// Links inside comment bodies or MOTD should not inherit tag/hall/user context
@@ -4162,6 +4165,25 @@ window.cancelAnimFrame = (function () {
})
.catch(console.error);
} else if (target.closest('#a_visibility') || target.closest('#info-visibility-edit-btn')) {
e.preventDefault();
const visTrigger = target.closest('#a_visibility') || target.closest('#info-visibility-edit-btn');
const id = visTrigger.dataset.itemId || document.getElementById('visibility-item-id')?.value;
const visBtnMain = document.getElementById('a_visibility');
const rawVis = visTrigger.dataset.visibility ?? visBtnMain?.dataset.visibility ?? '0';
const currentVis = parseInt(rawVis, 10);
const modal = document.getElementById('visibility-modal');
if (modal) {
const inputId = document.getElementById('visibility-item-id');
if (inputId && id) inputId.value = id;
const radios = modal.querySelectorAll('input[name="visibility"]');
radios.forEach(r => {
r.checked = (parseInt(r.value, 10) === currentVis);
});
modal.style.display = 'flex';
document.body.classList.add('modal-open');
}
} else if (target.closest('#a_rethumb')) {
e.preventDefault();
const reBtn = target.closest('#a_rethumb');
@@ -4530,8 +4552,10 @@ window.cancelAnimFrame = (function () {
const parts = p.split('/').filter(Boolean);
const isItem = !p.match(/\/p\//) && (
p.match(/^\/\d+/) ||
(parts.length >= 3 && parts[0] === 'tag' && /^\d+$/.test(parts[parts.length - 1])) ||
(parts.length >= 3 && parts[0] === 'user' && /^\d+$/.test(parts[parts.length - 1]))
p.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) ||
(parts.length >= 3 && parts[0] === 'tag' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
(parts.length >= 3 && parts[0] === 'user' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
(parts.length >= 3 && parts[0] === 'h' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1])))
);
const isSpecial = p.startsWith('/notifications') || p.startsWith('/tags') || p.startsWith('/user/') || p.startsWith('/subscriptions') || p.startsWith('/ranking');
const isGridLike = url.match(/\/p\/\d+/) || url.match(/[?&]page=\d+/) || p === '/';
@@ -8103,7 +8127,7 @@ class NotificationSystem {
}
}
if (typeof window.loadItemAjax === 'function' && href.match(/^\/\d+/)) {
if (typeof window.loadItemAjax === 'function' && (href.match(/^\/\d+/) || href.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) || href.match(/\/(?:user|tag|h)\/.*?\/(?:\d+|[a-zA-Z0-9_-]{11})/))) {
window.loadItemAjax(href, false);
} else if (typeof window.loadPageAjax === 'function') {
window.loadPageAjax(href, true);
@@ -8389,7 +8413,8 @@ class NotificationSystem {
}
renderHistoryItem(n) {
let link = `/${n.item_id}`;
const itemKey = n.item_slug || n.slug || n.item_id;
let link = `/${itemKey}`;
let msg = '';
let user = n.from_display_name || n.from_user || 'System';
@@ -8398,14 +8423,14 @@ class NotificationSystem {
const isDeleted = n.type === 'item_deleted';
const label = isDeleted ? (i18n.notif_upload_deleted || 'A moderator deleted your upload') : (i18n.notif_upload_denied || 'Your Upload was denied');
const userLabel = isDeleted ? (i18n.notif_moderation || 'Moderation') : (i18n.notif_system || 'System');
const itemLink = `/${n.item_id}`;
const itemLink = `/${itemKey}`;
return `
<a href="${itemLink}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
<div class="notif-content">
<div class="notif-user"><strong>${userLabel}</strong></div>
<div class="notif-msg">
<strong>${label} #${n.item_id}</strong>
<strong>${label} #${itemKey}</strong>
<div class="notif-reason">${i18n.notif_reason_label || 'Reason:'} ${n.reason || (i18n.notif_no_reason || 'No reason provided')}</div>
</div>
<div class="notif-time">${new Date(n.created_at).toLocaleString()}</div>
@@ -8425,7 +8450,7 @@ class NotificationSystem {
if (n.data?.msg) msg += ` <br><span style="color: #ff6060; font-size: 0.9em;">${n.data.msg}</span>`;
if (n.data?.url) msg += ` <br><small style="opacity: 0.6; word-break: break-all;">${n.data.url}</small>`;
user = (window.f0ckI18n && window.f0ckI18n.notif_system) || 'System';
link = n.item_id ? `/${n.item_id}` : '#';
link = n.item_id ? `/${itemKey}` : '#';
} else if (n.type === 'admin_pending') {
link = '/mod/approve';
user = (window.f0ckI18n && window.f0ckI18n.notif_admin) || 'Admin';
@@ -8441,7 +8466,7 @@ class NotificationSystem {
if (n.reason) msg += `<br><div class="notif-reason" style="font-size: 0.85em; color: #ffb8b8; margin-top: 3px;">${n.reason}</div>`;
} else {
// Comment notification
link = `/${n.item_id}#c${n.comment_id || n.reference_id}`;
link = `/${itemKey}#c${n.comment_id || n.reference_id}`;
if (n.type === 'comment_reply') msg = (window.f0ckI18n && window.f0ckI18n.notif_replied) || 'replied to you';
else if (n.type === 'subscription') msg = (window.f0ckI18n && window.f0ckI18n.notif_subscribed) || 'commented in a thread you follow';
else if (n.type === 'mention') msg = (window.f0ckI18n && window.f0ckI18n.notif_mentioned) || 'highlighted you';
@@ -8482,8 +8507,9 @@ class NotificationSystem {
}
renderItem(n) {
const itemKey = n.item_slug || n.slug || n.item_id;
if (n.type === 'approve') {
const link = `/${n.item_id}`;
const link = `/${itemKey}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div>
@@ -8498,7 +8524,7 @@ class NotificationSystem {
}
if (n.type === 'upload_success') {
const link = `/${n.item_id}`;
const link = `/${itemKey}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none';"></div>
@@ -8517,7 +8543,7 @@ class NotificationSystem {
const errMsg = n.data?.msg || '';
const errDisplay = errMsg ? `<div style="color: #ffbaba; font-size: 0.85em; margin-top: 2px;">${errMsg}</div>` : '';
const urlDisplay = url ? `<div style="font-size: 0.8em; opacity: 0.7; margin-top: 4px; word-break: break-all; max-height: 3.2em; overflow: hidden;">${url}</div>` : '';
const link = n.item_id ? `/${n.item_id}` : '#';
const link = n.item_id ? `/${itemKey}` : '#';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}">
<div class="notif-content">
@@ -8541,7 +8567,7 @@ class NotificationSystem {
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
<div class="notif-content">
<div>
<strong>${label} #${n.item_id}</strong>
<strong>${label} #${itemKey}</strong>
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 3px;">${(window.f0ckI18n && window.f0ckI18n.notif_click_reason) || 'Click to see reason'}</div>
</div>
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
@@ -8600,10 +8626,10 @@ class NotificationSystem {
if (n.type === 'comment_reply') typeText = (window.f0ckI18n && window.f0ckI18n.notif_replied) || 'replied to you';
else if (n.type === 'subscription') typeText = (window.f0ckI18n && window.f0ckI18n.notif_subscribed) || 'commented in a thread you follow';
else if (n.type === 'mention') typeText = (window.f0ckI18n && window.f0ckI18n.notif_mentioned) || 'highlighted you';
else if (n.type === 'upload_comment') typeText = `${(window.f0ckI18n && window.f0ckI18n.notif_commented) || 'commented on your upload'} #${n.item_id}`;
else if (n.type === 'upload_comment') typeText = `${(window.f0ckI18n && window.f0ckI18n.notif_commented) || 'commented on your upload'} #${itemKey}`;
const cid = n.comment_id || n.reference_id;
const link = `/${n.item_id}#c${cid}`;
const link = `/${itemKey}#c${cid}`;
const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none'"></div>` : '';
return `
@@ -8768,10 +8794,11 @@ class NotificationSystem {
// Build DOM nodes imperatively (avoids Sanitizer stripping inline border-color)
const fragment = document.createDocumentFragment();
data.favs.forEach(fav => {
if (fav.hide_fav_badge) {
const isSelf = window.f0ckSession && window.f0ckSession.user && (window.f0ckSession.user === fav.user);
if (fav.hide_fav_badge && !isSelf) {
const a = document.createElement('a');
a.className = 'ghost-fav';
a.setAttribute('tooltip', 'Ghost Fav');
a.setAttribute('tooltip', '?');
a.setAttribute('flow', 'up');
a.style.cursor = 'default';
@@ -8884,12 +8911,14 @@ class NotificationSystem {
if (!isOwnUpload) return; // Silently drop as before for other users' uploads
const itemKey = data.slug || data.id;
// Don't add duplicates
if (grid.querySelector(`a[href$="/${data.id}"]`)) return;
if (grid.querySelector(`a[href$="/${itemKey}"]`) || grid.querySelector(`a[href$="/${data.id}"]`)) return;
// Determine the link prefix from existing items
const firstThumb = grid.querySelector('a.thumb, a.lazy-thumb');
const linkBase = firstThumb ? firstThumb.getAttribute('href').replace(/\d+$/, '') : '/';
const linkBase = firstThumb ? firstThumb.getAttribute('href').replace(/(\d+|[a-zA-Z0-9_-]{11})$/, '') : '/';
// Build filter-reason label
const i18n = window.f0ckI18n || {};
@@ -8904,7 +8933,7 @@ class NotificationSystem {
const mode = data.tag_id ? (data.tag_id === 1 ? 'sfw' : (data.tag_id === 2 ? 'nsfw' : (data.tag_id == nsflId ? 'nsfl' : 'null'))) : 'null';
const ghost = document.createElement('a');
ghost.href = `${linkBase}${data.id}`;
ghost.href = `${linkBase}${itemKey}`;
ghost.className = 'thumb lazy-thumb filtered-upload-ghost loaded';
ghost.dataset.file = data.dest;
ghost.dataset.mime = data.mime;
@@ -8935,19 +8964,21 @@ class NotificationSystem {
}
}
const itemKey = data.slug || data.id;
// Don't add duplicates
if (grid.querySelector(`a[href$="/${data.id}"]`)) return;
if (grid.querySelector(`a[href$="/${itemKey}"]`) || grid.querySelector(`a[href$="/${data.id}"]`)) return;
// Determine the link prefix from existing items
const firstThumb = grid.querySelector('a.thumb, a.lazy-thumb');
const linkBase = firstThumb ? firstThumb.getAttribute('href').replace(/\d+$/, '') : '/';
const linkBase = firstThumb ? firstThumb.getAttribute('href').replace(/(\d+|[a-zA-Z0-9_-]{11})$/, '') : '/';
// Respect mode filter
const nsflId = window.f0ckSession?.nsfl_tag_id;
const mode = data.tag_id ? (data.tag_id === 1 ? 'sfw' : (data.tag_id === 2 ? 'nsfw' : (data.tag_id == nsflId ? 'nsfl' : 'null'))) : 'null';
const thumb = document.createElement('a');
thumb.href = `${linkBase}${data.id}`;
thumb.href = `${linkBase}${itemKey}`;
thumb.className = 'thumb lazy-thumb';
thumb.dataset.file = data.dest;
thumb.dataset.mime = data.mime;
@@ -9129,10 +9160,11 @@ class NotificationSystem {
</div>`;
}
const itemKey = c.item_slug || c.slug || c.item_id;
itemPreview = `
<div class="item-preview" style="margin-top: 10px; display: flex; align-items: center; gap: 10px; background: rgba(0,0,0,0.2); padding: 5px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.05);">
<a href="/${c.item_id}">${mediaHtml}</a>
<a href="/${c.item_id}#c${c.id}" style="font-size: 0.8em; color: var(--accent); text-decoration: none;">View Context &raquo;</a>
<a href="/${itemKey}">${mediaHtml}</a>
<a href="/${itemKey}#c${c.id}" style="font-size: 0.8em; color: var(--accent); text-decoration: none;">View Context &raquo;</a>
</div>`;
}
@@ -11302,6 +11334,83 @@ document.addEventListener('click', (e) => {
return;
}
const visCancelBtn = e.target.closest('#visibility-modal-cancel');
if (visCancelBtn || e.target.id === 'visibility-modal') {
const modal = document.getElementById('visibility-modal');
if (modal) {
modal.style.display = 'none';
document.body.classList.remove('modal-open');
}
return;
}
const visSaveBtn = e.target.closest('#visibility-modal-save');
if (visSaveBtn) {
e.preventDefault();
const modal = document.getElementById('visibility-modal');
const inputId = document.getElementById('visibility-item-id');
const selectedRadio = modal ? modal.querySelector('input[name="visibility"]:checked') : null;
if (!inputId || !selectedRadio) return;
const id = inputId.value;
const nextVis = parseInt(selectedRadio.value, 10);
visSaveBtn.disabled = true;
const origText = visSaveBtn.textContent;
visSaveBtn.textContent = 'Saving...';
fetch('/api/v2/item/visibility', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': window.f0ckSession?.csrf_token
},
body: new URLSearchParams({ postid: id, id: id, visibility: nextVis })
})
.then(r => r.json())
.then(data => {
visSaveBtn.disabled = false;
visSaveBtn.textContent = origText;
if (data.success) {
const visVal = parseInt(data.visibility, 10);
const titles = ['Public', 'Unlisted', 'Private'];
const icons = ['fa-globe', 'fa-link', 'fa-lock'];
const colors = ['var(--color-success, #00C851)', 'var(--color-warning, #ffbb33)', 'var(--color-danger, #ff4444)'];
const visBtn = document.getElementById('a_visibility');
if (visBtn) {
visBtn.dataset.visibility = visVal;
visBtn.setAttribute('title', `Visibility: ${titles[visVal]} (Click to change)`);
}
const infoVisBtn = document.getElementById('info-visibility-edit-btn');
if (infoVisBtn) {
infoVisBtn.dataset.visibility = visVal;
}
const infoVisLabel = document.getElementById('info-visibility-label');
if (infoVisLabel) {
infoVisLabel.innerHTML = `<i class="fa-solid ${icons[visVal]}" style="color: ${colors[visVal]};"></i> ${titles[visVal]}`;
}
if (modal) {
modal.style.display = 'none';
document.body.classList.remove('modal-open');
}
if (window.flashMessage) window.flashMessage(`VISIBILITY SET TO ${titles[visVal].toUpperCase()}`);
} else {
if (window.flashError) window.flashError(data.msg || 'Failed to update visibility');
}
})
.catch(err => {
visSaveBtn.disabled = false;
visSaveBtn.textContent = origText;
console.error('Error changing visibility:', err);
if (window.flashError) window.flashError('Network error');
});
return;
}
// Title save button
const saveBtn = e.target.closest('#info-title-save');
if (saveBtn) {

View File

@@ -1181,6 +1181,35 @@
});
}
const defaultUploadVisSelect = document.getElementById('default_upload_visibility_select');
if (defaultUploadVisSelect) {
defaultUploadVisSelect.addEventListener('change', async function() {
const vis = parseInt(defaultUploadVisSelect.value, 10);
try {
const res = await fetch('/api/v2/settings/default_upload_visibility', {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': window.f0ckSession ? window.f0ckSession.csrf_token : ''
},
body: new URLSearchParams({ default_upload_visibility: vis })
});
const data = await res.json();
if (data.success) {
showStatus('Default upload visibility updated!', 'success');
if (window.f0ckSession) {
window.f0ckSession.default_upload_visibility = vis;
}
} else {
alert(data.msg || 'Error saving preference');
}
} catch (err) {
console.error('Update Default Upload Visibility error:', err);
alert('Connection error');
}
});
}
// New Dual Column Layout Toggle
const layoutToggle = document.getElementById('use_new_layout_toggle');
if (layoutToggle) {

View File

@@ -479,8 +479,9 @@
};
const renderActivityItem = (c) => {
const itemKey = c.item_slug || c.slug || c.item_id;
const rawContent = c.content || c.body || '';
let displayContent = renderCommentContent(rawContent, c.id, c.item_id);
let displayContent = renderCommentContent(rawContent, c.id, itemKey);
displayContent = window.f0cklib?.processMentions ? window.f0cklib.processMentions(displayContent) : displayContent;
@@ -497,7 +498,7 @@
}
const attachmentsHtml = renderCommentAttachments(c.files, rawContent);
const pollHtml = renderSidebarPoll(c.poll, c.id, c.item_id);
const pollHtml = renderSidebarPoll(c.poll, c.id, itemKey);
// Build avatar URL — same priority as the rest of the app
let avatarSrc = '/a/default.png';
@@ -542,8 +543,8 @@
itemPreview = `
<div class="item-preview">
<a href="/${c.item_id}" class="sidebar-thumb-link" data-mode="${rClass}">${mediaHtml}</a>
<a href="/${c.item_id}#c${c.id}" style="font-size: 0.8em; color: var(--accent); text-decoration: none;">${(window.f0ckI18n && window.f0ckI18n.sidebar_view) || 'View'} &raquo;</a>
<a href="/${itemKey}" class="sidebar-thumb-link" data-mode="${rClass}">${mediaHtml}</a>
<a href="/${itemKey}#c${c.id}" style="font-size: 0.8em; color: var(--accent); text-decoration: none;">${(window.f0ckI18n && window.f0ckI18n.sidebar_view) || 'View'} &raquo;</a>
</div>`;
}

View File

@@ -908,7 +908,7 @@ window.initUploadForm = (selector) => {
}
lines.forEach(url => {
if (!selectedFiles.some(item => item.type === 'url' && item.url === url)) {
selectedFiles.push({ type: 'url', url, rating: '', tags: [], comment: '', title: '', is_oc: false });
selectedFiles.push({ type: 'url', url, rating: '', visibility: '', tags: [], comment: '', title: '', is_oc: false });
}
});
urlInput.value = '';
@@ -931,7 +931,7 @@ window.initUploadForm = (selector) => {
const val = urlInput.value.trim();
if (!val || !/^https?:\/\//i.test(val)) return;
if (!selectedFiles.some(item => item.type === 'url' && item.url === val)) {
selectedFiles.push({ type: 'url', url: val, rating: '', tags: [], comment: '', title: '', is_oc: false });
selectedFiles.push({ type: 'url', url: val, rating: '', visibility: '', tags: [], comment: '', title: '', is_oc: false });
}
urlInput.value = '';
if (urlBadge) urlBadge.style.display = 'none';
@@ -1177,7 +1177,7 @@ window.initUploadForm = (selector) => {
if (!selectedFiles.some(f => (f.file || f).name === file.name && (f.file || f).size === file.size)) {
if (isShitpost) {
selectedFiles.push({ type: 'file', file: file, rating: '', tags: [], comment: '', title: '', is_oc: false });
selectedFiles.push({ type: 'file', file: file, rating: '', visibility: '', tags: [], comment: '', title: '', is_oc: false });
} else {
selectedFiles.push(file); // Legacy single file mode uses raw File
}
@@ -1490,6 +1490,7 @@ window.initUploadForm = (selector) => {
infoRow.className = 'file-meta-row-small';
let ratingSwitch = '';
let visibilitySwitch = '';
let tagsUI = '';
let ocUI = '';
let commentUI = '';
@@ -1517,6 +1518,26 @@ window.initUploadForm = (selector) => {
</div>
`;
const globalVis = form.querySelector('input[name="visibility"]:checked')?.value || '0';
const visValue = (item.visibility !== undefined && item.visibility !== '') ? item.visibility : globalVis;
item.visibility = visValue;
visibilitySwitch = `
<div class="item-visibility-container item-rating-container" style="margin-top: 4px;">
<label class="item-rating-option">
<input type="radio" name="visibility_${index}" value="0" ${visValue === '0' || visValue === 0 ? 'checked' : ''}>
<span class="item-rating-label sfw" style="display: inline-flex; align-items: center; gap: 4px;"><i class="fa-solid fa-globe"></i> Public</span>
</label>
<label class="item-rating-option">
<input type="radio" name="visibility_${index}" value="1" ${visValue === '1' || visValue === 1 ? 'checked' : ''}>
<span class="item-rating-label nsfw" style="display: inline-flex; align-items: center; gap: 4px;"><i class="fa-solid fa-link"></i> Unlisted</span>
</label>
<label class="item-rating-option">
<input type="radio" name="visibility_${index}" value="2" ${visValue === '2' || visValue === 2 ? 'checked' : ''}>
<span class="item-rating-label nsfl" style="display: inline-flex; align-items: center; gap: 4px;"><i class="fa-solid fa-lock"></i> Private</span>
</label>
</div>
`;
const tagsPlaceholder = window.f0ckI18n?.upload_tags_placeholder || 'Tags...';
const minTagsHint = shitpostMinTags > 0 ? ` (min ${shitpostMinTags})` : '';
tagsUI = `
@@ -1560,19 +1581,27 @@ window.initUploadForm = (selector) => {
</div>
${titleUI}
${ratingSwitch}
${visibilitySwitch}
${tagsUI}
${commentUI}
`;
if (isShitpost) {
// Handle Rating
infoRow.querySelectorAll('.item-rating-option input').forEach(radio => {
infoRow.querySelectorAll('.item-rating-container:not(.item-visibility-container) input').forEach(radio => {
radio.onchange = () => {
item.rating = radio.value;
updateSubmitButton();
};
});
// Handle Visibility
infoRow.querySelectorAll('.item-visibility-container input').forEach(radio => {
radio.onchange = () => {
item.visibility = radio.value;
};
});
// Handle Comment
const commentInput = infoRow.querySelector('.item-comment-input');
const emojiTrigger = infoRow.querySelector('.item-emoji-trigger');
@@ -2436,6 +2465,8 @@ window.initUploadForm = (selector) => {
}
try {
const globalVisEl = form.querySelector('input[name="visibility"]:checked');
const visibilityVal = globalVisEl ? globalVisEl.value : '0';
const resp = await fetch('/api/v2/upload-url', {
method: 'POST',
headers: {
@@ -2445,7 +2476,8 @@ window.initUploadForm = (selector) => {
},
body: JSON.stringify({
url,
rating: globalRatingEl.value,
rating: globalRatingEl ? globalRatingEl.value : 'sfw',
visibility: visibilityVal,
tags: tags.join(','),
comment: comment,
is_oc: isOc,
@@ -2552,9 +2584,11 @@ window.initUploadForm = (selector) => {
for (let i = 0; i < selectedFiles.length; i++) {
const item = selectedFiles[i];
const globalVisEl = form.querySelector('input[name="visibility"]:checked');
const isUrlItem = isShitpost && item.type === 'url';
const file = !isUrlItem ? (isShitpost ? item.file : item) : null;
const fileRating = isShitpost ? item.rating : (globalRatingEl ? globalRatingEl.value : 'sfw');
const fileVisibility = isShitpost ? (item.visibility || globalVisEl?.value || '0') : (globalVisEl?.value || '0');
const fileTags = isShitpost ? item.tags : tags;
const fileComment = isShitpost ? item.comment : comment;
const fileTitle = isShitpost ? (item.title || '') : titleVal;
@@ -2571,6 +2605,7 @@ window.initUploadForm = (selector) => {
formData.append('file', file);
}
formData.append('rating', fileRating);
formData.append('visibility', fileVisibility);
formData.append('tags', fileTags.join(','));
formData.append('is_oc', (isShitpost ? item.is_oc : isOc) ? 'true' : 'false');
if (isShitpost) formData.append('is_shitpost', 'true');
@@ -2622,6 +2657,7 @@ window.initUploadForm = (selector) => {
xhr.send(JSON.stringify({
url: item.url,
rating: fileRating,
visibility: fileVisibility,
tags: fileTags.join(','),
is_oc: (isShitpost ? item.is_oc : isOc),
comment: fileComment,
@@ -2738,17 +2774,14 @@ window.initUploadForm = (selector) => {
// Skip redirect if every item was a background URL job
const allPending = lastData?.pending && selectedFiles.every(i => i.type === 'url');
if (!allPending) {
// Inject now if the grid is already in the DOM (upload modal open on main page)
injectNewItem();
// Navigate to main page, then inject again after the grid has loaded.
// Awaiting loadPageAjax ensures the .posts grid DOM is present before the
// handleNewItem call — this covers the item-page drag-and-upload scenario
// where no grid exists until after navigation completes.
const targetUrl = (lastData && (lastData.visibility > 0 || lastData.redirect || lastData.slug))
? (lastData.redirect || `/${lastData.slug || lastData.itemid}`)
: '/';
if (typeof window.loadPageAjax === 'function') {
await window.loadPageAjax('/', true, { bypassCache: true });
injectNewItem();
await window.loadPageAjax(targetUrl, true, { bypassCache: true });
if (targetUrl === '/') injectNewItem();
} else {
window.location.href = '/';
window.location.href = targetUrl;
}
}
} else {

View File

@@ -1,14 +1,20 @@
(async () => {
// Helper to get dynamic context from the DOM
const getContext = () => {
const idLink = document.querySelector("a.id-link");
if (!idLink) return null;
const commentsEl = document.querySelector("#comments-container");
const favoEl = document.querySelector("#a_favo");
const infoEl = document.querySelector("#a_info");
const idLinkEl = document.querySelector("a.id-link");
const rawId = commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText;
if (!rawId) return null;
const tagsContainer = document.querySelector("#tags");
const inner = tagsContainer.querySelector(".tags-inner") || tagsContainer;
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
return {
postid: +idLink.innerText,
postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(),
poster: document.querySelector("a#a_username")?.innerText,
tags: [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2))
tags: inner ? [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2)) : []
};
};

View File

@@ -410,11 +410,12 @@ if (!window.UserCommentSystem) {
}
renderComment(c) {
const itemKey = c.item_slug || c.slug || c.item_id;
const timeAgo = this.timeAgo(c.created_at);
const fullDate = new Date(c.created_at).toISOString();
const content = this.renderCommentContent(c.content, c.item_id);
const content = this.renderCommentContent(c.content, itemKey);
return `<div class="comment" id="c${c.id}"><div class="comment-avatar"><a href="/${c.item_id}"><img src="/t/${c.item_id}.webp" alt=""></a></div><div class="comment-body"><div class="comment-header"><div class="comment-header-left"><span class="comment-author" tooltip="ID: ${c.user_id}" ${this.userColor ? `style="color: ${this.userColor}"` : ''}>${this.username}</span></div><span class="comment-time timeago" title="${fullDate}">${timeAgo}</span></div><div class="comment-content" data-raw="${this.escapeHtml(c.content)}">${content}</div>${this.renderCommentAttachments(c.files, c.content)}${this.renderCommentPoll(c.poll, c.id)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${window.f0ckSession && window.f0ckSession.logged_in ? `<button class="report-comment-btn" data-id="${c.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 512 512" fill="currentColor"><path d="M506.3 417l-213.3-364c-16.3-28-57.5-28-73.8 0l-213.2 364C-10.6 445.1 9.7 480 42.7 480h426.6C502.5 480 522.6 445.1 506.3 417zM256 384c-14.1 0-25.6-11.5-25.6-25.6 0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6C281.6 372.5 270.1 384 256 384zM281.6 264.4c0 14.1-11.5 25.6-25.6 25.6-14.1 0-25.6-11.5-25.6-25.6v-96c0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6V264.4z"/></svg></button>` : ''}</div></div></div></div><a href="/${c.item_id}#c${c.id}" class="comment-permalink" title="Permalink">#${c.id}</a></div>`;
return `<div class="comment" id="c${c.id}"><div class="comment-avatar"><a href="/${itemKey}"><img src="/t/${c.item_id}.webp" alt=""></a></div><div class="comment-body"><div class="comment-header"><div class="comment-header-left"><span class="comment-author" tooltip="ID: ${c.user_id}" ${this.userColor ? `style="color: ${this.userColor}"` : ''}>${this.username}</span></div><span class="comment-time timeago" title="${fullDate}">${timeAgo}</span></div><div class="comment-content" data-raw="${this.escapeHtml(c.content)}">${content}</div>${this.renderCommentAttachments(c.files, c.content)}${this.renderCommentPoll(c.poll, c.id)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${window.f0ckSession && window.f0ckSession.logged_in ? `<button class="report-comment-btn" data-id="${c.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 512 512" fill="currentColor"><path d="M506.3 417l-213.3-364c-16.3-28-57.5-28-73.8 0l-213.2 364C-10.6 445.1 9.7 480 42.7 480h426.6C502.5 480 522.6 445.1 506.3 417zM256 384c-14.1 0-25.6-11.5-25.6-25.6 0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6C281.6 372.5 270.1 384 256 384zM281.6 264.4c0 14.1-11.5 25.6-25.6 25.6-14.1 0-25.6-11.5-25.6-25.6v-96c0-14.1 11.5-25.6 25.6-25.6 14.1 0 25.6 11.5 25.6 25.6V264.4z"/></svg></button>` : ''}</div></div></div></div><a href="/${itemKey}#c${c.id}" class="comment-permalink" title="Permalink">#${c.id}</a></div>`;
}
startLiveTimestamps() {