diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js
index f831b5a..83269dd 100644
--- a/public/s/js/f0ckm.js
+++ b/public/s/js/f0ckm.js
@@ -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 += `
${thumb}
diff --git a/public/s/js/upload.js b/public/s/js/upload.js
index b5c2ffe..74dbdc2 100644
--- a/public/s/js/upload.js
+++ b/public/s/js/upload.js
@@ -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';
}
- } 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(() => {
diff --git a/src/inc/routes/notifications.mjs b/src/inc/routes/notifications.mjs
index 3c0d274..db8d0d1 100644
--- a/src/inc/routes/notifications.mjs
+++ b/src/inc/routes/notifications.mjs
@@ -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\/(?\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));
});
diff --git a/src/inc/routes/warnings.mjs b/src/inc/routes/warnings.mjs
index 78a5a27..7303a70 100644
--- a/src/inc/routes/warnings.mjs
+++ b/src/inc/routes/warnings.mjs
@@ -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,
diff --git a/views/notifications.html b/views/notifications.html
index b7060c3..3bc3332 100644
--- a/views/notifications.html
+++ b/views/notifications.html
@@ -7,10 +7,10 @@
-
-
+
+
-
+
@include(snippets/notifications-list)
@if(pagination.next)
diff --git a/views/snippets/notifications-list.html b/views/snippets/notifications-list.html
index 7e4bd97..3a4defe 100644
--- a/views/snippets/notifications-list.html
+++ b/views/snippets/notifications-list.html
@@ -103,6 +103,18 @@
{{ new Date(n.created_at).toLocaleString() }}
+@elseif(n.type === 'warning')
+