QoL fixes

This commit is contained in:
2026-06-03 12:25:57 +02:00
parent 5bb86f7028
commit d30642ca4a
8 changed files with 110 additions and 39 deletions

View File

@@ -283,6 +283,8 @@ window.cancelAnimFrame = (function () {
baseStyle += 'background:rgba(200,30,30,0.95);';
} else if (type === 'success') {
baseStyle += 'background:rgba(30,130,60,0.95);';
} else if (type === 'warning') {
baseStyle += 'background:rgba(220,180,0,0.95);color:#000;';
} else {
baseStyle += 'background:rgba(30,30,30,0.95);';
}
@@ -5985,7 +5987,7 @@ if (sbtForm) {
class NotificationSystem {
// Notification type categorization
static USER_TYPES = ['comment_reply', 'subscription', 'mention', 'upload_comment'];
static SYSTEM_TYPES = ['approve', 'deny', 'item_deleted', 'upload_success', 'upload_error', 'admin_pending', 'report'];
static SYSTEM_TYPES = ['approve', 'deny', 'item_deleted', 'upload_success', 'upload_error', 'admin_pending', 'report', 'warning'];
constructor() {
this.bell = document.getElementById('nav-notif-btn');
@@ -6185,7 +6187,7 @@ class NotificationSystem {
// System notifications (deletion, deny, reports) require explicit acknowledgment —
// never auto-mark them as read just because the user is viewing that item.
const isSystemNotif = ['item_deleted', 'deny', 'admin_pending', 'report'].includes(notifType);
const isSystemNotif = ['item_deleted', 'deny', 'admin_pending', 'report', 'warning'].includes(notifType);
// If the user is currently viewing this item, mark comment-type notifications as read immediately
// (they are live on the thread, so no need to show a badge/highlight)
@@ -6950,6 +6952,11 @@ class NotificationSystem {
link = '/mod/reports';
user = (window.f0ckI18n && window.f0ckI18n.notif_moderation) || 'Moderator';
msg = (window.f0ckI18n && window.f0ckI18n.notif_new_report) || 'A new user report has been submitted';
} else if (n.type === 'warning') {
link = `/notifications?tab=system#notif-${n.id}`;
user = (window.f0ckI18n && window.f0ckI18n.notif_system) || 'System';
msg = (window.f0ckI18n && window.f0ckI18n.account_warning && window.f0ckI18n.account_warning.title) || 'Account Warning';
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}`;
@@ -6961,7 +6968,18 @@ class NotificationSystem {
// For admin_pending the thumbnail lives in /mod/pending/t/ until approved
let thumbSrc, thumbOnerror;
if (n.type === 'admin_pending') {
if (n.type === 'warning') {
return `
<div class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="notif-content">
<div class="notif-user"><strong ${n.username_color ? `style="color: ${n.username_color}"` : ''}>${user}</strong></div>
<div class="notif-msg">${msg}</div>
<div class="notif-time">${new Date(n.created_at).toLocaleString()}</div>
</div>
</div>
`;
} else if (n.type === 'admin_pending') {
thumbSrc = `/mod/pending/t/${n.item_id}.webp`;
thumbOnerror = `this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}`;
} else {
@@ -7080,6 +7098,22 @@ class NotificationSystem {
`;
}
if (n.type === 'warning') {
const link = `/notifications?tab=system#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" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="notif-content">
<div>
<strong>${(window.f0ckI18n && window.f0ckI18n.account_warning && window.f0ckI18n.account_warning.title) || 'Account Warning'}</strong>
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 3px;">${n.reason || 'No reason provided'}</div>
</div>
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
</div>
</a>
`;
}
let typeText = 'Start';
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';

View File

@@ -543,6 +543,14 @@
const result = await res.json();
if (result.success) {
if (result.manual_approval) {
window.flashMessage(window.f0ckI18n?.upload_pending_approval_patient || 'Upload awaits approval', 3000, 'warning');
if (window.loadPageAjax) {
window.loadPageAjax('/');
} else {
window.location.href = '/';
}
} else {
const dest = result.redirect || '/meme';
if (window.loadItemAjax) {
window.loadItemAjax(dest);
@@ -552,6 +560,7 @@
window.location.href = dest;
}
}
}
else {
window.flashMessage('Error: ' + result.msg, 3000, 'error');
uploadBtn.disabled = false;

View File

@@ -3496,7 +3496,7 @@
// Tab type arrays
const SCROLLER_USER_TYPES = ['comment_reply', 'subscription', 'mention', 'upload_comment'];
const SCROLLER_SYSTEM_TYPES = ['approve', 'deny', 'item_deleted', 'upload_success', 'upload_error', 'admin_pending', 'report'];
const SCROLLER_SYSTEM_TYPES = ['approve', 'deny', 'item_deleted', 'upload_success', 'upload_error', 'admin_pending', 'report', 'warning'];
let sActiveTab = 'user';
let sCachedNotifs = [];
@@ -3600,6 +3600,9 @@
} else if (n.type === 'report') {
link = '/mod/reports'; user = i18n.notif_moderation || 'Moderator';
msg = i18n.notif_new_report || 'New user report';
} else if (n.type === 'warning') {
link = `/notifications?tab=system#notif-${n.id}`; user = i18n.notif_system || 'System';
msg = (i18n.account_warning && i18n.account_warning.title) || 'Account Warning';
} else {
link = `/${n.item_id}#c${n.reference_id}`;
if (n.type === 'comment_reply') msg = i18n.notif_replied || 'replied to you';
@@ -3607,8 +3610,13 @@
else if (n.type === 'mention') msg = i18n.notif_mentioned || 'highlighted you';
else msg = i18n.notif_commented || 'commented';
}
let thumb;
if (n.type === 'warning') {
thumb = `<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>`;
} else {
const thumbSrc = n.type === 'admin_pending' ? `/mod/pending/t/${n.item_id}.webp` : `/t/${n.item_id}.webp`;
const thumb = n.item_id ? `<div class="notif-thumb"><img src="${thumbSrc}" alt="" onerror="this.style.display='none'"></div>` : '';
thumb = n.item_id ? `<div class="notif-thumb"><img src="${thumbSrc}" alt="" onerror="this.style.display='none'"></div>` : '';
}
return `<a href="${link}" target="_blank" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}">
${thumb}
<div class="notif-content">

View File

@@ -1971,18 +1971,18 @@ window.initUploadForm = (selector) => {
form._f0ckUploader.reset();
if (isShitpost) {
// Flash message removed as requested
if (lastData?.manual_approval && typeof window.showFlash === 'function') {
window.showFlash('Upload awaits approval, please be patient', 'info');
if (lastData?.manual_approval && typeof window.flashMessage === 'function') {
window.flashMessage(window.f0ckI18n?.upload_pending_approval_patient || 'Upload awaits approval', 3000, 'warning');
}
} else {
if (!dragModal && statusDiv) {
if (lastData?.manual_approval) {
if (typeof window.flashMessage === 'function') {
window.flashMessage(window.f0ckI18n?.upload_pending_approval_patient || 'Upload awaits approval', 3000, 'warning');
}
} else if (!dragModal && statusDiv) {
statusDiv.innerHTML = '✓ ' + (lastData?.msg || 'Upload successful');
statusDiv.className = 'upload-status success';
}
if (lastData?.manual_approval && typeof window.showFlash === 'function') {
window.showFlash('Upload awaits approval, please be patient', 'info');
}
}
setTimeout(() => {
@@ -2127,17 +2127,18 @@ window.initUploadForm = (selector) => {
if (dragModal) dragModal.classList.remove('show');
form._f0ckUploader.reset();
if (isShitpost) {
// Flash message removed as requested
if (lastData?.manual_approval && typeof window.showFlash === 'function') {
window.showFlash('Upload awaits approval, please be patient', 'info');
if (lastData?.manual_approval && typeof window.flashMessage === 'function') {
window.flashMessage(window.f0ckI18n?.upload_pending_approval_patient || 'Upload awaits approval', 3000, 'warning');
}
} else {
if (lastData?.manual_approval) {
if (typeof window.flashMessage === 'function') {
window.flashMessage(window.f0ckI18n?.upload_pending_approval_patient || 'Upload awaits approval', 3000, 'warning');
}
} else if (!dragModal && statusDiv) {
statusDiv.innerHTML = '✓ ' + (lastData?.msg || 'Upload successful');
statusDiv.className = 'upload-status success';
}
if (!isShitpost && lastData?.manual_approval && typeof window.showFlash === 'function') {
window.showFlash('Upload awaits approval, please be patient', 'info');
}
setTimeout(() => {

View File

@@ -57,7 +57,8 @@ db.listen('notifications', (payload) => {
if (client.do_not_disturb === true) continue;
if (SYSTEM_TYPES.includes(data.type) && client.receive_system_notifications === false) continue;
if (USER_TYPES.includes(data.type) && client.receive_user_notifications === false) continue;
// warnings bypass user settings
if (data.type !== 'warning' && USER_TYPES.includes(data.type) && client.receive_user_notifications === false) continue;
client.send({ type: 'notify', data });
}
}
@@ -343,7 +344,7 @@ db.listen('global_chat_topic', (payload) => {
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'];
const SYSTEM_TYPES = ['approve', 'deny', 'item_deleted', 'upload_success', 'upload_error', 'admin_pending', 'report', 'warning'];
async function getNotificationHistory(userId, page = 1, limit = 50, tab = null) {
const offset = (page - 1) * limit;
@@ -363,7 +364,7 @@ export default (router, tpl) => {
LEFT JOIN items i ON n.item_id = i.id
WHERE n.user_id = ${userId}
AND n.type = ANY(${typeFilter})
AND (n.type IN ('admin_pending', 'deny', 'item_deleted', 'report') OR i.id IS NULL OR (i.active = true AND i.is_deleted = false))
AND (n.type IN ('admin_pending', 'deny', 'item_deleted', 'report', 'warning') OR i.id IS NULL OR (i.active = true AND i.is_deleted = false))
ORDER BY n.created_at DESC
LIMIT ${limit + 1}
OFFSET ${offset}
@@ -381,7 +382,7 @@ export default (router, tpl) => {
LEFT JOIN user_options uo ON u.id = uo.user_id
LEFT JOIN items i ON n.item_id = i.id
WHERE n.user_id = ${userId}
AND (n.type IN ('admin_pending', 'deny', 'item_deleted', 'report') OR i.id IS NULL OR (i.active = true AND i.is_deleted = false))
AND (n.type IN ('admin_pending', 'deny', 'item_deleted', 'report', 'warning') OR i.id IS NULL OR (i.active = true AND i.is_deleted = false))
ORDER BY n.created_at DESC
LIMIT ${limit + 1}
OFFSET ${offset}
@@ -426,7 +427,7 @@ export default (router, tpl) => {
LEFT JOIN user_options uo ON u.id = uo.user_id
LEFT JOIN items i ON n.item_id = i.id
WHERE n.user_id = ${req.session.id} AND n.is_read = false
AND (n.type IN ('admin_pending', 'deny', 'item_deleted', 'report', 'approve')
AND (n.type IN ('admin_pending', 'deny', 'item_deleted', 'report', 'approve', 'warning')
OR (
${req.session.do_not_disturb !== true} AND (
(n.type IN ('upload_success', 'upload_error') AND ${req.session.receive_system_notifications !== false})
@@ -434,7 +435,7 @@ export default (router, tpl) => {
)
)
)
AND (n.item_id IS NULL OR (i.active = true AND i.is_deleted = false) OR n.type IN ('admin_pending', 'deny', 'item_deleted', 'report'))
AND (n.item_id IS NULL OR (i.active = true AND i.is_deleted = false) OR n.type IN ('admin_pending', 'deny', 'item_deleted', 'report', 'warning'))
ORDER BY n.created_at DESC
LIMIT 1000
`;
@@ -496,7 +497,7 @@ export default (router, tpl) => {
router.post(/\/api\/notifications\/item\/(?<itemId>\d+)\/read/, async (req, res) => {
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false }) });
const itemId = req.params.itemId;
const SYSTEM_TYPES = ['item_deleted', 'deny', 'admin_pending', 'report'];
const SYSTEM_TYPES = ['item_deleted', 'deny', 'admin_pending', 'report', 'warning'];
console.log(`[NotificationRoute] Marking comment notifications for item ${itemId} as read for user ${req.session.id}`);
try {
await db`
@@ -646,6 +647,7 @@ export default (router, tpl) => {
next: data.hasMore ? 2 : null
};
data.link = { main: '/notifications', path: '/' };
data.activeTab = tab;
data.domain = cfg.main.url.domain; // For header
return res.html(tpl.render('notifications', data, req));
});

View File

@@ -21,6 +21,11 @@ export default (router, tpl) => {
// Broadcast to SSE clients instantly
if (result.length > 0) {
await db`
INSERT INTO notifications (user_id, type, reference_id, data, is_read)
VALUES (${+user_id}, 'warning', 0, ${JSON.stringify({ reason: reason.trim(), warning_id: result[0].id })}, false)
`;
await db`SELECT pg_notify('warnings', ${JSON.stringify({
user_id: +user_id,
warning_id: result[0].id,

View File

@@ -7,10 +7,10 @@
<button id="mark-all-read-page" class="btn-small">{{ t('notifications.mark_all_read') }}</button>
</div>
<div class="notif-page-tabs">
<button class="notif-page-tab active" data-tab="user">{{ t('nav.notif_tab_user') }}</button>
<button class="notif-page-tab" data-tab="system">{{ t('nav.notif_tab_system') }}</button>
<button class="notif-page-tab @if(activeTab === 'user') active @endif" data-tab="user">{{ t('nav.notif_tab_user') }}</button>
<button class="notif-page-tab @if(activeTab === 'system') active @endif" data-tab="system">{{ t('nav.notif_tab_system') }}</button>
</div>
<div id="notifications-container" class="posts notifications-list-full" data-page="{{ pagination.page }}" data-tab="user">
<div id="notifications-container" class="posts notifications-list-full" data-page="{{ pagination.page }}" data-tab="{{ activeTab }}">
@include(snippets/notifications-list)
</div>
@if(pagination.next)

View File

@@ -103,6 +103,18 @@
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@elseif(n.type === 'warning')
<div class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
<div class="notif-msg">
<strong>Account Warning</strong>
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 4px;">Reason: {{ n.reason }}</div>
</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</div>
@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)