diff --git a/config_example.json b/config_example.json
index 3fc91b6..15746db 100644
--- a/config_example.json
+++ b/config_example.json
@@ -97,6 +97,7 @@
],
"show_mime_picker": true,
"embed_youtube_in_comments": true,
+ "allow_comment_deletion": false,
"show_content_warning": true,
"default_comment_display_mode": 1,
"phrases": [
diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css
index 57029e2..1308a2a 100644
--- a/public/s/css/f0ckm.css
+++ b/public/s/css/f0ckm.css
@@ -2261,7 +2261,8 @@ body.layout-modern .item-sidebar-left .tag-controls {
.admin-pin-btn,
.admin-edit-btn,
-.admin-delete-btn {
+.admin-delete-btn,
+.delete-btn {
background: none;
border: none;
padding: 0;
@@ -2271,7 +2272,8 @@ body.layout-modern .item-sidebar-left .tag-controls {
text-decoration: underline;
}
-.admin-delete-btn:hover {
+.admin-delete-btn:hover,
+.delete-btn:hover {
color: #ff4444;
}
@@ -2799,7 +2801,8 @@ body.layout-legacy .scroll-to-bottom svg {
}
-.admin-delete-btn:hover {
+.admin-delete-btn:hover,
+.delete-btn:hover {
color: #ff4444 !important;
}
diff --git a/public/s/js/comments.js b/public/s/js/comments.js
index 90c65e3..1ea4f2e 100644
--- a/public/s/js/comments.js
+++ b/public/s/js/comments.js
@@ -1891,6 +1891,13 @@ class CommentSystem {
adminButtons = ``;
}
+ let userDeleteButton = '';
+ if (!this.isAdmin && !isDeleted && window.f0ckSession?.logged_in && window.f0ckSession?.allow_comment_deletion) {
+ if (comment.username && comment.username === window.f0ckSession.user) {
+ userDeleteButton = ``;
+ }
+ }
+
const pinnedBadge = isPinned ? `${this.icons.pinned}` : '';
const commentClass = isReply ? 'comment reply' : 'comment';
@@ -1938,6 +1945,7 @@ class CommentSystem {
@@ -2433,27 +2441,48 @@ class CommentSystem {
// User Delete
const delBtn = target.closest('.delete-btn');
if (delBtn) {
- if (delBtn.dataset.confirming !== 'true') {
- delBtn.dataset.confirming = 'true';
- const originalText = delBtn.innerHTML;
- delBtn.innerHTML = 'Sure?';
- delBtn.classList.add('btn-danger'); // Optional styling
- setTimeout(() => {
- delBtn.dataset.confirming = 'false';
- delBtn.innerHTML = originalText;
- delBtn.classList.remove('btn-danger');
- }, 3000);
- return;
- }
-
const id = delBtn.dataset.id;
- const res = await fetch(`/api/comments/${id}/delete`, {
- method: 'POST',
- headers: { 'X-CSRF-Token': window.f0ckSession?.csrf_token }
- });
- const json = await res.json();
- if (json.success) this.loadComments();
- else alert('Failed to delete: ' + (json.message || 'Error'));
+ ModAction.confirm('Delete Comment', `Are you sure you want to delete comment ${id}?`, async () => {
+ const res = await fetch(`/api/comments/${id}/delete`, {
+ method: 'POST',
+ headers: {
+ 'x-csrf-token': window.f0ckSession?.csrf_token
+ }
+ });
+ const json = await res.json();
+ if (json.success) {
+ const sidebarEl = document.getElementById('sc' + id);
+ if (sidebarEl) {
+ sidebarEl.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
+ sidebarEl.style.opacity = '0';
+ sidebarEl.style.transform = 'scale(0.95)';
+ setTimeout(() => sidebarEl.remove(), 300);
+ }
+ if (window._sidebarActivityCache) {
+ window._sidebarActivityCache = window._sidebarActivityCache.filter(c => String(c.id) !== String(id));
+ }
+
+ const commentEl = document.getElementById(`c${id}`);
+ if (commentEl) {
+ commentEl.classList.add('deleted');
+ const contentEl = commentEl.querySelector('.comment-content');
+ if (contentEl) {
+ contentEl.innerHTML = '[deleted]';
+ }
+ const actionsEl = commentEl.querySelector('.comment-actions');
+ if (actionsEl) {
+ actionsEl.innerHTML = ''; // Remove reply/quote/admin buttons
+ }
+ // Remove attachments if present
+ const attachmentsEl = commentEl.querySelector('.comment-attachments, .media-pills');
+ if (attachmentsEl) attachmentsEl.remove();
+ } else {
+ this.loadComments();
+ }
+ } else {
+ throw new Error(json.message || 'Failed to delete');
+ }
+ }, { hideReason: true });
return;
}
@@ -2474,6 +2503,17 @@ class CommentSystem {
});
const json = await res.json();
if (json.success) {
+ const sidebarEl = document.getElementById('sc' + id);
+ if (sidebarEl) {
+ sidebarEl.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
+ sidebarEl.style.opacity = '0';
+ sidebarEl.style.transform = 'scale(0.95)';
+ setTimeout(() => sidebarEl.remove(), 300);
+ }
+ if (window._sidebarActivityCache) {
+ window._sidebarActivityCache = window._sidebarActivityCache.filter(c => String(c.id) !== String(id));
+ }
+
const commentEl = document.getElementById(`c${id}`);
if (commentEl) {
commentEl.classList.add('deleted');
diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js
index 7ea5518..84b9f06 100644
--- a/public/s/js/f0ckm.js
+++ b/public/s/js/f0ckm.js
@@ -5764,6 +5764,17 @@ class NotificationSystem {
if (contentEl) contentEl.innerHTML = '[deleted]';
}
});
+ // Also vanish from sidebar
+ const sidebarEl = document.getElementById('sc' + data.data.comment_id);
+ if (sidebarEl) {
+ sidebarEl.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
+ sidebarEl.style.opacity = '0';
+ sidebarEl.style.transform = 'scale(0.95)';
+ setTimeout(() => sidebarEl.remove(), 300);
+ }
+ if (window._sidebarActivityCache) {
+ window._sidebarActivityCache = window._sidebarActivityCache.filter(c => String(c.id) !== String(data.data.comment_id));
+ }
} else if (data.data.type === 'edit') {
if (window.commentSystem && typeof window.commentSystem.handleLiveEdit === 'function') {
window.commentSystem.handleLiveEdit(data.data);
diff --git a/src/inc/routes/comments.mjs b/src/inc/routes/comments.mjs
index 0e36930..389ebb6 100644
--- a/src/inc/routes/comments.mjs
+++ b/src/inc/routes/comments.mjs
@@ -543,8 +543,14 @@ export default (router, tpl) => {
const comment = await db`SELECT content, item_id, user_id FROM comments WHERE id = ${commentId}`;
if (!comment.length) return res.reply({ code: 404, body: JSON.stringify({ success: false, message: "Not found" }) });
- if (!req.session.admin && !req.session.is_moderator && comment[0].user_id !== req.session.id) {
- return res.reply({ code: 403, body: JSON.stringify({ success: false, message: "Forbidden" }) });
+ const { getAllowCommentDeletion } = await import("../settings.mjs");
+ const canDeleteOwn = getAllowCommentDeletion();
+ const isOwner = comment[0].user_id === req.session.id;
+
+ if (!req.session.admin && !req.session.is_moderator) {
+ if (!canDeleteOwn || !isOwner) {
+ return res.reply({ code: 403, body: JSON.stringify({ success: false, message: "Forbidden" }) });
+ }
}
// Log all deletions in audit log
diff --git a/src/inc/settings.mjs b/src/inc/settings.mjs
index 85fa7d6..387007a 100644
--- a/src/inc/settings.mjs
+++ b/src/inc/settings.mjs
@@ -80,3 +80,7 @@ export const setLogUserIps = (val) => {}; // No-op, strictly config-based
export const getHashUserIps = () => !!cfg.websrv.hash_user_ips;
export const setHashUserIps = (val) => {}; // No-op, strictly config-based
+
+export const getAllowCommentDeletion = () => !!cfg.websrv.allow_comment_deletion;
+export const setAllowCommentDeletion = (val) => {}; // No-op, strictly config-based
+
diff --git a/src/index.mjs b/src/index.mjs
index b2737b4..f73965a 100644
--- a/src/index.mjs
+++ b/src/index.mjs
@@ -18,7 +18,7 @@ import { handleMetaExtract } from "./meta_extract_handler.mjs";
import { handleMetaStrip } from "./meta_strip_handler.mjs";
import { handleCommentUpload } from "./comment_upload_handler.mjs";
import { handleDmAttachmentUpload, handleDmAttachmentDownload, handleDmAttachmentDelete } from "./dm_attachment_handler.mjs";
-import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDmAttachments, setDmAttachments, getDmUnencrypted, setDmUnencrypted, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode } from "./inc/settings.mjs";
+import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDmAttachments, setDmAttachments, getDmUnencrypted, setDmUnencrypted, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode, getAllowCommentDeletion, setAllowCommentDeletion } from "./inc/settings.mjs";
import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs";
import { createI18n } from "./inc/i18n.mjs";
import security from "./inc/security.mjs";
@@ -1114,6 +1114,7 @@ process.on('uncaughtException', err => {
get private_messages() { return getPrivateMessages(); },
get dm_attachments() { return getDmAttachments(); },
get dm_unencrypted() { return getDmUnencrypted(); },
+ get allow_comment_deletion() { return getAllowCommentDeletion(); },
get enable_pdf() { return getEnablePdf(); },
get enable_cleanup() { return getEnableCleanup(); },
get cleanup_start_date() { return getCleanupStartDate(); },
diff --git a/views/snippets/footer.html b/views/snippets/footer.html
index 916306c..83d0950 100644
--- a/views/snippets/footer.html
+++ b/views/snippets/footer.html
@@ -1,4 +1,4 @@
-@if(session && (session.admin || session.is_moderator))
+@if(session)
{{ t('mod.confirm_action') }}
@@ -138,7 +138,7 @@
@endif
@if(private_society && !session)