notification thumbnails according to rating blurred or unblurred

This commit is contained in:
2026-06-04 15:44:57 +02:00
parent 39dfcad52f
commit e281484b8a
4 changed files with 154 additions and 22 deletions

View File

@@ -15685,3 +15685,81 @@ body.scroller-active #gchat-reopen-bubble {
opacity: 0 !important;
pointer-events: none !important;
}
/* =============================================
NOTIFICATION THUMBNAIL BLUR
Applies the same blur treatment to .notif-thumb
when the corresponding blur-active class is set.
============================================= */
.blur-nsfw-active .notif-thumb[data-mode="nsfw"] img,
.blur-nsfl-active .notif-thumb[data-mode="nsfl"] img,
.blur-sfw-active .notif-thumb[data-mode="sfw"] img,
.blur-untagged-active .notif-thumb[data-mode="untagged"] img {
filter: blur(8px) !important;
transform: scale(1.08) !important;
transition: filter 0.2s ease, transform 0.2s ease !important;
}
/* Reveal on hover — only when hovering directly on the thumb */
.notif-thumb[data-mode]:hover img {
filter: none !important;
transform: scale(1.1) !important;
}
/* Overlay label */
.blur-nsfw-active .notif-thumb[data-mode="nsfw"],
.blur-nsfl-active .notif-thumb[data-mode="nsfl"],
.blur-sfw-active .notif-thumb[data-mode="sfw"],
.blur-untagged-active .notif-thumb[data-mode="untagged"] {
position: relative !important;
}
.blur-nsfw-active .notif-thumb[data-mode="nsfw"]::after,
.blur-nsfl-active .notif-thumb[data-mode="nsfl"]::after,
.blur-sfw-active .notif-thumb[data-mode="sfw"]::after,
.blur-untagged-active .notif-thumb[data-mode="untagged"]::after {
content: attr(data-mode) !important;
text-transform: uppercase !important;
position: absolute !important;
inset: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-weight: 800 !important;
font-size: 10px !important;
letter-spacing: 1.5px !important;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9) !important;
background: rgba(10, 10, 12, 0.55) !important;
pointer-events: none !important;
opacity: 1 !important;
transition: opacity 0.2s ease !important;
z-index: 2 !important;
}
.blur-nsfw-active .notif-thumb[data-mode="nsfw"]::after { color: #ffffff !important; }
.blur-nsfl-active .notif-thumb[data-mode="nsfl"]::after { color: #ff3b30 !important; }
.blur-sfw-active .notif-thumb[data-mode="sfw"]::after { color: #30d158 !important; }
.blur-untagged-active .notif-thumb[data-mode="untagged"]::after { color: #ff9f0a !important; }
/* Hide overlay on hover — only when hovering directly on the thumb */
.notif-thumb[data-mode]:hover::after {
opacity: 0 !important;
}
/* Revealed state (set by JS tap-to-reveal for mobile) */
.notif-thumb[data-mode].revealed img {
filter: none !important;
}
.notif-thumb[data-mode].revealed::after {
opacity: 0 !important;
pointer-events: none !important;
}
/* ALL mode: suppress all notification thumbnail blur immediately */
html.mode-all .notif-thumb[data-mode] img {
filter: none !important;
transform: none !important;
}
html.mode-all .notif-thumb[data-mode]::after {
display: none !important;
}

View File

@@ -504,6 +504,16 @@ window.cancelAnimFrame = (function () {
}
}
// Reflect ALL mode on <html> immediately (controls notif-thumb blur suppression)
if (window.activeMode === 3) htmlEl.classList.add('mode-all');
document.addEventListener('f0ck:modeChanged', (e) => {
if (e.detail?.mode === 3) {
htmlEl.classList.add('mode-all');
} else {
htmlEl.classList.remove('mode-all');
}
});
// ---- Multi-select Rating Toggles ----
// Reads/writes a `ratings` cookie (e.g. "sfw|untagged") and syncs with server via /mode/3 (ALL).
const getRatingsCookie = () => {
@@ -6567,9 +6577,33 @@ class NotificationSystem {
// Single Notification Click Handler (Delegated)
// Handles both Dropdown and History Page
const handleNotificationClick = (e) => {
// Blur-reveal intercept: if the tap/click landed on (or inside) a blurred
// notif-thumb that hasn't been revealed yet, reveal it and stop navigation.
// Skip entirely in ALL mode — nothing is blurred.
const tappedThumb = e.target.closest('.notif-thumb[data-mode]');
if (tappedThumb && !tappedThumb.classList.contains('revealed') && window.activeMode !== 3) {
const mode = tappedThumb.dataset.mode;
const blurNsfw = localStorage.getItem('blurNsfw') === 'true';
const blurNsfl = localStorage.getItem('blurNsfl') === 'true';
const blurSfw = localStorage.getItem('blurSfw') === 'true';
const blurUntagged = localStorage.getItem('blurUntagged') === 'true';
const isBlurred =
(mode === 'nsfw' && blurNsfw) ||
(mode === 'nsfl' && blurNsfl) ||
(mode === 'sfw' && blurSfw) ||
(mode === 'untagged' && blurUntagged);
if (isBlurred) {
e.preventDefault();
e.stopPropagation();
tappedThumb.classList.add('revealed');
return;
}
}
const link = e.target.closest('.notif-item');
if (!link) return;
// Handle "Mark as Read"
if (link.dataset.id && link.classList.contains('unread')) {
window.f0ckDebug(`[NotificationSystem] Marking ${link.dataset.id} as read...`);
@@ -6919,7 +6953,7 @@ class NotificationSystem {
const itemLink = `/${n.item_id}`;
return `
<a href="${itemLink}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb"><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-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">
@@ -6986,7 +7020,7 @@ class NotificationSystem {
thumbSrc = `/t/${n.item_id}.webp`;
thumbOnerror = `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';};}`;
}
const thumb = n.item_id ? `<div class="notif-thumb"><img src="${thumbSrc}" alt="thumb" onerror="${thumbOnerror}"></div>` : '';
const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="${thumbSrc}" alt="thumb" onerror="${thumbOnerror}"></div>` : '';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
${thumb}
@@ -7004,7 +7038,7 @@ class NotificationSystem {
const link = `/${n.item_id}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb"><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>
<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>
<div class="notif-content">
<div>
<strong>${(window.f0ckI18n && window.f0ckI18n.notif_upload_approved) || 'Your Upload has been approved'}</strong>
@@ -7019,7 +7053,7 @@ class NotificationSystem {
const link = `/${n.item_id}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb"><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none';"></div>
<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>
<div class="notif-content">
<div>
<strong>${(window.f0ckI18n && window.f0ckI18n.notif_upload_success) || 'Your background upload is finished!'}</strong>
@@ -7056,7 +7090,7 @@ class NotificationSystem {
const link = `/notifications#notif-${n.id}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-notif-nav="true">
<div class="notif-thumb"><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-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>
@@ -7072,7 +7106,7 @@ class NotificationSystem {
const link = '/mod/approve';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb"><img src="/mod/pending/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-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/pending/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>${(window.f0ckI18n && window.f0ckI18n.notif_upload_pending) || 'A new upload needs approval'}</strong>
@@ -7087,7 +7121,7 @@ class NotificationSystem {
const link = '/mod/reports';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb"><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>
<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>
<div class="notif-content">
<div>
<strong>${(window.f0ckI18n && window.f0ckI18n.notif_new_report) || 'A new user report has been submitted'}</strong>
@@ -7123,7 +7157,7 @@ class NotificationSystem {
const cid = n.comment_id || n.reference_id;
const link = `/${n.item_id}#c${cid}`;
const thumb = n.item_id ? `<div class="notif-thumb"><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none'"></div>` : '';
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 `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
${thumb}

View File

@@ -346,6 +346,8 @@ export default (router, tpl) => {
const USER_TYPES = ['comment_reply', 'subscription', 'mention', 'upload_comment'];
const SYSTEM_TYPES = ['approve', 'deny', 'item_deleted', 'upload_success', 'upload_error', 'admin_pending', 'report', 'warning'];
const nsflTagId = cfg.nsfl_tag_id || 3;
async function getNotificationHistory(userId, page = 1, limit = 50, tab = null) {
const offset = (page - 1) * limit;
const typeFilter = tab === 'system' ? SYSTEM_TYPES : (tab === 'user' ? USER_TYPES : null);
@@ -356,7 +358,13 @@ export default (router, tpl) => {
COALESCE(uo.display_name, '') as from_display_name,
COALESCE(u.id, 0) as from_user_id,
uo.username_color,
i.dest, i.mime
i.dest, i.mime,
CASE (SELECT ta.tag_id FROM tags_assign ta WHERE ta.item_id = n.item_id AND ta.tag_id IN (1, 2, ${nsflTagId}) LIMIT 1)
WHEN 1 THEN 'sfw'
WHEN 2 THEN 'nsfw'
WHEN ${nsflTagId} THEN 'nsfl'
ELSE NULL
END as item_mode
FROM notifications n
LEFT JOIN comments c ON n.reference_id = c.id
LEFT JOIN "user" u ON c.user_id = u.id
@@ -375,7 +383,13 @@ export default (router, tpl) => {
COALESCE(uo.display_name, '') as from_display_name,
COALESCE(u.id, 0) as from_user_id,
uo.username_color,
i.dest, i.mime
i.dest, i.mime,
CASE (SELECT ta.tag_id FROM tags_assign ta WHERE ta.item_id = n.item_id AND ta.tag_id IN (1, 2, ${nsflTagId}) LIMIT 1)
WHEN 1 THEN 'sfw'
WHEN 2 THEN 'nsfw'
WHEN ${nsflTagId} THEN 'nsfl'
ELSE NULL
END as item_mode
FROM notifications n
LEFT JOIN comments c ON n.reference_id = c.id
LEFT JOIN "user" u ON c.user_id = u.id
@@ -420,7 +434,13 @@ export default (router, tpl) => {
COALESCE(uo.display_name, '') as from_display_name,
COALESCE(u.id, 0) as from_user_id,
uo.username_color,
i.dest, i.mime
i.dest, i.mime,
CASE (SELECT ta.tag_id FROM tags_assign ta WHERE ta.item_id = n.item_id AND ta.tag_id IN (1, 2, ${nsflTagId}) LIMIT 1)
WHEN 1 THEN 'sfw'
WHEN 2 THEN 'nsfw'
WHEN ${nsflTagId} THEN 'nsfl'
ELSE NULL
END as item_mode
FROM notifications n
LEFT JOIN comments c ON n.reference_id = c.id
LEFT JOIN "user" u ON c.user_id = u.id

View File

@@ -1,8 +1,8 @@
@each(notifications as n)
@if(n.type === 'approve')
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<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 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>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
@@ -12,7 +12,7 @@
</a>
@elseif(n.type === 'admin_pending')
<a href="/mod/approve" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<div class="notif-thumb" {{ n.item_mode ? `data-mode="${n.item_mode}"` : '' }}>
<img src="/mod/pending/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">
@@ -23,8 +23,8 @@
</a>
@elseif(n.type === 'report')
<a href="/mod/reports" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<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 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>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.moderation') }}</strong></div>
@@ -34,7 +34,7 @@
</a>
@elseif(n.type === 'deny')
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<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">
@@ -48,7 +48,7 @@
</a>
@elseif(n.type === 'item_deleted')
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<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">
@@ -62,7 +62,7 @@
</a>
@elseif(n.type === 'upload_comment')
<a href="/{{ n.item_id }}#c{{ n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<div class="notif-thumb" {{ n.item_mode ? `data-mode="${n.item_mode}"` : '' }}>
<img src="/t/{{ n.item_id }}.webp" alt="thumbnail" onerror="this.style.display='none'">
</div>
<div class="notif-content">
@@ -73,7 +73,7 @@
</a>
@elseif(n.type === 'upload_success')
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<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>
<div class="notif-content">
@@ -85,7 +85,7 @@
@elseif(n.type === 'upload_error')
<a href="{{ n.item_id ? '/' + n.item_id : '#' }}" class="notif-item {{ n.is_read ? '' : 'unread' }} {{ n.item_id ? 'notif-with-thumb' : '' }}" data-id="{{ n.id }}">
@if(n.item_id)
<div class="notif-thumb">
<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.style.display='none'};" />
</div>
@endif
@@ -118,7 +118,7 @@
@else
<a href="/{{ n.item_id }}#c{{ n.comment_id || n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
@if(n.item_id)
<div class="notif-thumb">
<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>
@endif