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

@@ -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) {