diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index bdaefc6..37bf49e 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -6732,6 +6732,39 @@ span#favs { font-size: 15px; } +._error_filter_hint { + grid-column: 1 / -1; + margin-top: 12px; + padding: 8px 14px; + border: 1px dashed rgba(242, 239, 11, 0.35); + background: rgba(242, 239, 11, 0.06); + color: rgba(255, 255, 255, 0.75); + font-size: 0.82em; + line-height: 1.5; + text-align: center; + text-shadow: none; +} + +._error_filter_hint i { + margin-right: 5px; + opacity: 0.7; +} + +._error_filter_hint strong { + color: var(--accent, #f2ef0b); + font-weight: 700; +} + +._error_filter_hint a { + color: var(--accent, #f2ef0b); + text-decoration: underline; + text-underline-offset: 2px; +} + +._error_filter_hint a:hover { + opacity: 0.8; +} + /* Admin search css early test */ .admin-search { margin-top: 15px; @@ -15933,3 +15966,87 @@ body.scroller-active #gchat-reopen-bubble { .archive-download-btn i { font-size: 1em; } + +/* ── Filtered-upload ghost thumbnail ────────────────────────────────────────── + Shown when the current user's freshly-uploaded item is hidden by an active + content filter (SFW / NSFW / NSFL mode). Disappears on manual page reload. + ──────────────────────────────────────────────────────────────────────────── */ +a.thumb.filtered-upload-ghost, +a.lazy-thumb.filtered-upload-ghost { + position: relative; + overflow: hidden; + cursor: pointer; + border: 2px dashed rgba(255, 255, 255, 0.25) !important; + box-sizing: border-box; +} + +/* Strongly blur the background thumbnail image */ +a.thumb.filtered-upload-ghost::before, +a.lazy-thumb.filtered-upload-ghost::before { + content: ''; + position: absolute; + inset: -6px; /* bleed out so blur edges don't show */ + background: var(--thumb-bg) center / cover no-repeat; + filter: blur(14px) brightness(0.45); + z-index: 0; +} + +/* Dark overlay to give the label good contrast */ +a.thumb.filtered-upload-ghost::after, +a.lazy-thumb.filtered-upload-ghost::after { + content: ''; + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 1; +} + +/* Hover: slightly brighten to hint it's clickable */ +a.thumb.filtered-upload-ghost:hover::before, +a.lazy-thumb.filtered-upload-ghost:hover::before { + filter: blur(14px) brightness(0.6); +} + +a.thumb.filtered-upload-ghost:hover::after, +a.lazy-thumb.filtered-upload-ghost:hover::after { + background: rgba(0, 0, 0, 0.3); +} + +/* Overlay label inside the ghost thumb */ +.filtered-upload-ghost-label { + position: absolute; + inset: 0; + z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + padding: 8px; + text-align: center; + pointer-events: none; + color: rgba(255, 255, 255, 0.92); + user-select: none; +} + +.filtered-upload-ghost-label .fug-icon { + font-size: 1.4em; + opacity: 0.85; + line-height: 1; +} + +.filtered-upload-ghost-label .fug-title { + font-size: 0.65em; + font-weight: 700; + letter-spacing: 0.04em; + line-height: 1.25; + text-transform: uppercase; + opacity: 0.95; +} + +.filtered-upload-ghost-label .fug-hint { + font-size: 0.58em; + opacity: 0.6; + line-height: 1.3; + font-weight: 400; +} diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 1af7dd6..5c5266d 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -676,11 +676,14 @@ window.cancelAnimFrame = (function () { window.flashMessage('ALL MODE ACTIVATED'); gridCacheMap.clear(); const isGridView = document.querySelector('.posts, .tags-grid'); + const isItemView = document.getElementById('prev') || document.getElementById('next') || /^\/\d+/.test(window.location.pathname); let reloadPromise = null; if (isGridView) { const currentUrl = new URL(window.location.href); currentUrl.searchParams.delete('mode'); - reloadPromise = loadPageAjax(currentUrl.toString(), true, { skipCache: true }); + reloadPromise = window.loadPageAjax ? window.loadPageAjax(currentUrl.toString(), true, { skipCache: true }) : null; + } else if (isItemView) { + reloadPromise = window.loadItemAjax ? window.loadItemAjax(window.location.href, true, { skipCache: true }) : null; } if (fromFilterModal) Promise.resolve(reloadPromise).finally(() => { window._keepFilterModal = false; }); } @@ -731,14 +734,15 @@ window.cancelAnimFrame = (function () { window.flashMessage(label); gridCacheMap.clear(); const isGridView = document.querySelector('.posts, .tags-grid'); - const isItemView = document.getElementById('prev') || document.getElementById('next'); + const isItemView = document.getElementById('prev') || document.getElementById('next') || /^\/\d+/.test(window.location.pathname); let reloadPromise = null; if (isGridView) { const currentUrl = new URL(window.location.href); currentUrl.searchParams.delete('mode'); - reloadPromise = loadPageAjax(currentUrl.toString(), true, { skipCache: true }); + reloadPromise = window.loadPageAjax ? window.loadPageAjax(currentUrl.toString(), true, { skipCache: true }) : null; } else if (isItemView) { updateNavForMode(window.activeMode); + reloadPromise = window.loadItemAjax ? window.loadItemAjax(window.location.href, true, { skipCache: true }) : null; } if (fromFilterModal) Promise.resolve(reloadPromise).finally(() => { window._keepFilterModal = false; }); } else { @@ -5495,6 +5499,14 @@ window.cancelAnimFrame = (function () { }); } + // Delegated handler: [data-action="open-filter-modal"] links (e.g. error page hint) + document.addEventListener('click', (e) => { + const trigger = e.target.closest('[data-action="open-filter-modal"]'); + if (!trigger) return; + e.preventDefault(); + toggleModal(true); + }); + close.addEventListener('click', () => toggleModal(false)); overlay.addEventListener('click', (e) => { if (e.target === overlay) toggleModal(false); @@ -7957,12 +7969,74 @@ class NotificationSystem { window.location.pathname.includes('/h/') || window.location.pathname.includes('/user/')) return; - // Respect mode filter + // Respect mode filter — but if this is the current user's own upload, + // show a blurred "ghost" thumbnail so they know it was uploaded successfully + // even though the active filter hides it from the grid. if (typeof window.activeMode !== 'undefined' && window.activeMode !== 3) { // 3 is ALL - if (window.activeMode === 0 && data.tag_id !== 1) return; // SFW mode, item is not SFW - if (window.activeMode === 1 && data.tag_id !== 2) return; // NSFW mode, item is not NSFW - if (window.activeMode === 4 && data.tag_id != window.f0ckSession?.nsfl_tag_id) return; // NSFL mode, item is not NSFL - if (window.activeMode === 2) return; // Untagged mode, new uploads are never untagged + let filtered = false; + let filterReason = ''; + if (window.activeMode === 0 && data.tag_id !== 1) { filtered = true; filterReason = 'SFW'; } // SFW mode, item is not SFW + else if (window.activeMode === 1 && data.tag_id !== 2) { filtered = true; filterReason = 'NSFW'; } // NSFW mode, item is not NSFW + else if (window.activeMode === 4 && data.tag_id != window.f0ckSession?.nsfl_tag_id) { filtered = true; filterReason = 'NSFL'; } // NSFL mode + else if (window.activeMode === 2) { filtered = true; filterReason = 'untagged'; } + + if (filtered) { + // Check if this is the current user's own upload + const sessionUser = (window.f0ckSession?.user || '').toLowerCase(); + const itemUser = (data.username || '').toLowerCase(); + const isOwnUpload = sessionUser && itemUser && sessionUser === itemUser; + + if (!isOwnUpload) return; // Silently drop as before for other users' uploads + + // Don't add duplicates + if (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+$/, '') : '/'; + + // Build filter-reason label + const i18n = window.f0ckI18n || {}; + const filterLabel = filterReason === 'SFW' ? (i18n.filter_mode_sfw || 'SFW filter active') + : filterReason === 'NSFW' ? (i18n.filter_mode_nsfw || 'NSFW filter active') + : filterReason === 'NSFL' ? (i18n.filter_mode_nsfl || 'NSFL filter active') + : (i18n.filter_mode_other || 'Filter active'); + const ghostTitle = i18n.filtered_upload_title || 'Your upload'; + const ghostHint = i18n.filtered_upload_hint || 'Hidden by filter · click to view'; + + 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 ghost = document.createElement('a'); + ghost.href = `${linkBase}${data.id}`; + ghost.className = 'thumb lazy-thumb filtered-upload-ghost loaded'; + ghost.dataset.file = data.dest; + ghost.dataset.mime = data.mime; + ghost.dataset.user = data.display_name || data.username; + ghost.dataset.mode = mode; + ghost.style.setProperty('--thumb-bg', `url('/t/${data.id}.webp')`); + ghost.style.opacity = '0'; + ghost.style.transform = 'scale(0.9)'; + ghost.innerHTML = ` +
+ +
`; + + const pinnedItems = grid.querySelectorAll('.is-pinned'); + if (pinnedItems.length > 0) { + pinnedItems[pinnedItems.length - 1].after(ghost); + } else { + grid.prepend(ghost); + } + + requestAnimationFrame(() => { + ghost.style.transition = 'opacity 0.4s ease, transform 0.4s ease'; + ghost.style.opacity = '1'; + ghost.style.transform = 'scale(1)'; + }); + + return; // Ghost injected — done + } } // Don't add duplicates diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index b2dc192..22b485d 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -327,7 +327,10 @@ "scroll_for_more": "Scrollen für mehr..." }, "error": { - "label": "Fehler" + "label": "Fehler", + "post_not_visible": "Dieser Beitrag ist derzeit leider nicht sichtbar.", + "filter_hint": "Dein aktueller Filter ist auf {mode} gesetzt.", + "filter_hint_link": "Um diesen Inhalt zu sehen, ändere deinen Filter entsprechend." }, "drop": { "drop_anywhere": "Überall ablegen zum Hochladen" diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index b43da33..2a72881 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -327,7 +327,10 @@ "scroll_for_more": "Scroll for more..." }, "error": { - "label": "Error" + "label": "Error", + "post_not_visible": "Sorry, this post is currently not visible.", + "filter_hint": "Your current filter is set to {mode}.", + "filter_hint_link": "To view this content, change your filter accordingly." }, "drop": { "drop_anywhere": "Drop anywhere to upload" diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index 5a356cb..ac6194f 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -327,7 +327,10 @@ "scroll_for_more": "Scroll voor meer..." }, "error": { - "label": "Fout" + "label": "Fout", + "post_not_visible": "Sorry, deze post is momenteel niet zichtbaar.", + "filter_hint": "Je huidige filter is ingesteld op {mode}.", + "filter_hint_link": "Om deze inhoud te bekijken, pas je filter aan." }, "drop": { "drop_anywhere": "Overal slepen om te uploaden" diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index bd274aa..38cf679 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -325,7 +325,10 @@ "scroll_for_more": "Scrollen für mehr..." }, "error": { - "label": "Fehler" + "label": "Fehler", + "post_not_visible": "Bedauerlicher Weise ist dieser Pfosten derzeit nicht sichtbar.", + "filter_hint": "Ihr aktueller Filter ist auf {mode} eingestellt.", + "filter_hint_link": "Um diesen Inhalt zu betrachten, stellen Sie Ihren Filter entsprechend um." }, "drop": { "drop_anywhere": "Überall ablegen zum Aufladieren" diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs index 91b0c22..c59a5f0 100644 --- a/src/inc/routes/ajax.mjs +++ b/src/inc/routes/ajax.mjs @@ -1,6 +1,7 @@ import f0cklib from "../routeinc/f0cklib.mjs"; import url from "url"; import cfg from "../config.mjs"; +import { createI18n } from "../i18n.mjs"; export default (router, tpl) => { router.get(/\/ajax\/item\/(?\d+)/, async (req, res) => { @@ -57,9 +58,17 @@ export default (router, tpl) => { const tAjaxFetch = Date.now(); if (!data.success) { + const reqMode = (query.mode !== undefined ? +query.mode : req.mode) ?? 0; + const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Untagged', 4: 'NSFL' }; + const errorModeLabel = (req.session && reqMode !== 3) ? (modeLabels[reqMode] || null) : null; + const { t: tErr } = createI18n(req.session?.language || req.lang || 'en'); const errorHtml = tpl.render('error-partial', { - message: data.message || '404 - Not f0cked', - tmp: null + message: tErr('error.post_not_visible'), + tmp: null, + session: req.session ? { ...req.session } : false, + error_mode_label: errorModeLabel, + error_filter_hint: errorModeLabel ? tErr('error.filter_hint', { mode: `${errorModeLabel}` }) : null, + error_filter_hint_link: tErr('error.filter_hint_link') }, req); return res.reply({ headers: { 'Content-Type': 'application/json' }, diff --git a/src/inc/routes/index.mjs b/src/inc/routes/index.mjs index 72d8db5..b8fa523 100644 --- a/src/inc/routes/index.mjs +++ b/src/inc/routes/index.mjs @@ -2,6 +2,7 @@ import cfg from "../config.mjs"; import db from "../sql.mjs"; import lib from "../lib.mjs"; import f0cklib from "../routeinc/f0cklib.mjs"; +import { createI18n } from "../i18n.mjs"; const auth = async (req, res, next) => { if (!req.session) @@ -270,13 +271,21 @@ export default (router, tpl) => { // Return 200 for filtered NSFW items (has item data) so Discord parses og:image // Return 404 only for truly missing items const statusCode = data.item ? 200 : 404; + const reqMode = req.mode ?? 0; + const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Untagged', 4: 'NSFL' }; + const errorModeLabel = (req.session && reqMode !== 3) ? (modeLabels[reqMode] || null) : null; + const { t: tErr } = createI18n(req.session?.language || req.lang || 'en'); return res.reply({ code: statusCode, body: tpl.render('error', { - message: data.message, + message: tErr('error.post_not_visible'), item: data.item, // For OG meta tags on filtered NSFW items domain: cfg.main.url.domain, - tmp: null + tmp: null, + session: req.session ? { ...req.session } : false, + error_mode_label: errorModeLabel, + error_filter_hint: errorModeLabel ? tErr('error.filter_hint', { mode: `${errorModeLabel}` }) : null, + error_filter_hint_link: tErr('error.filter_hint_link') }, req) }); } diff --git a/views/error-partial.html b/views/error-partial.html index ad74a82..d0c256e 100644 --- a/views/error-partial.html +++ b/views/error-partial.html @@ -1,20 +1,22 @@ -
-
-
-
-
- (Ͼ˳Ͽ)..!!! -
-
- - ZOMG - -
- Error - {{ message }} -
-
+
+
+
+ (Ͼ˳Ͽ)..!!! +
+
+ + ZOMG + +
+ {{ t('error.label') }} + {{ message }}
+ @if(session && error_filter_hint) +
+ {{ error_filter_hint }}
+ {{ error_filter_hint_link }} +
+ @endif
diff --git a/views/error.html b/views/error.html index a2d9351..b485c19 100644 --- a/views/error.html +++ b/views/error.html @@ -14,6 +14,12 @@ {{ t('error.label') }} {{ message }}
+ @if(session && error_filter_hint) +
+ {{ error_filter_hint }}
+ {{ error_filter_hint_link }} +
+ @endif