From 3d88cb8696cc9a5d206575646ae952352e4d06f1 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 9 Jun 2025 15:26:58 +0000 Subject: [PATCH 01/12] chore: incrementing version number - v4.4.3 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index d4f1154359..fae65ae63f 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.4.2", + "version": "4.4.3", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From 0c9297f81cd1e62df4c6cecaf36fe66fc4016472 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 9 Jun 2025 15:26:59 +0000 Subject: [PATCH 02/12] chore: update changelog for v4.4.3 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8883eacb3..ec4faa3658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +#### v4.4.3 (2025-06-09) + +##### Chores + +* up composer (5f51dfc4) +* incrementing version number - v4.4.2 (55c510ae) +* update changelog for v4.4.2 (6d40a211) +* incrementing version number - v4.4.1 (5ae79b4e) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### Bug Fixes + +* escape, query params (b02eb57d) +* closes #13475, don't store escaped username (806e54bf) + #### v4.4.2 (2025-06-02) ##### Chores From 14e30c4bf8b0c3064cd1b3789badf32f50e22cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 10:47:14 -0400 Subject: [PATCH 03/12] feat: closes #13484, post preview changes don't close preview when mouse leaves the anchor close preview on click outside close preview when mouseleaves preview open the preview to the top if there isn't enough space add scrollbar to post preview --- public/src/client/topic.js | 28 ++++++++++++++++++++--- src/views/partials/topic/post-preview.tpl | 15 +++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 1919638f01..b790560fa1 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -312,12 +312,27 @@ define('forum/topic', [ const postCache = {}; function destroyTooltip() { clearTimeout(timeoutId); + timeoutId = 0; $('#post-tooltip').remove(); destroyed = true; } + + function onClickOutside(ev) { + // If the click is outside the tooltip, destroy it + if (!$(ev.target).closest('#post-tooltip').length) { + destroyTooltip(); + } + } + $(window).one('action:ajaxify.start', destroyTooltip); - $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/content"] a, [component="topic/event"] a', async function () { + $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () { const link = $(this); + link.one('mouseleave', function() { + if (timeoutId > 0) { + clearTimeout(timeoutId); + timeoutId = 0; + } + }); destroyed = false; async function renderPost(pid) { @@ -335,8 +350,13 @@ define('forum/topic', [ const postRect = postContent.offset(); const postWidth = postContent.width(); const linkRect = link.offset(); + const { top } = link.get(0).getBoundingClientRect(); + const dropup = top > window.innerHeight / 2; + + tooltip.one('mouseleave', destroyTooltip); + $(window).off('click', onClickOutside).one('click', onClickOutside); tooltip.css({ - top: linkRect.top + 30, + top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, left: postRect.left, width: postWidth, }); @@ -357,16 +377,18 @@ define('forum/topic', [ } timeoutId = setTimeout(async () => { + timeoutId = 0; renderPost(pid); }, 300); } else if (topicMatch) { timeoutId = setTimeout(async () => { + timeoutId = 0; const tid = topicMatch[1]; const topicData = await api.get('/topics/' + tid, {}); renderPost(topicData.mainPid); }, 300); } - }).on('mouseleave', '[component="post"] a, [component="topic/event"] a', destroyTooltip); + }); } function setupQuickReply() { diff --git a/src/views/partials/topic/post-preview.tpl b/src/views/partials/topic/post-preview.tpl index 107075eef3..c307e14f81 100644 --- a/src/views/partials/topic/post-preview.tpl +++ b/src/views/partials/topic/post-preview.tpl @@ -1,11 +1,14 @@
From 8ab034d8f05c90e03fef0f719c2110061d0de01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 10:52:55 -0400 Subject: [PATCH 04/12] lint: fix lint --- public/src/client/topic.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index b790560fa1..0480d3844c 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -327,7 +327,7 @@ define('forum/topic', [ $(window).one('action:ajaxify.start', destroyTooltip); $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () { const link = $(this); - link.one('mouseleave', function() { + link.one('mouseleave', function () { if (timeoutId > 0) { clearTimeout(timeoutId); timeoutId = 0; @@ -356,7 +356,7 @@ define('forum/topic', [ tooltip.one('mouseleave', destroyTooltip); $(window).off('click', onClickOutside).one('click', onClickOutside); tooltip.css({ - top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, + top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, left: postRect.left, width: postWidth, }); From 0ebb31fe87750e6b50a2cccc389c0170543af493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 12:39:49 -0400 Subject: [PATCH 05/12] fix: #13484, clear tooltip if cursor leaves link and doesn't enter tooltip --- public/src/client/topic.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 0480d3844c..3bb50a5da5 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -309,12 +309,14 @@ define('forum/topic', [ } let timeoutId = 0; let destroyed = false; + let cursorOnPreviewTooltip = false; const postCache = {}; function destroyTooltip() { clearTimeout(timeoutId); timeoutId = 0; $('#post-tooltip').remove(); destroyed = true; + cursorOnPreviewTooltip = false; } function onClickOutside(ev) { @@ -328,6 +330,14 @@ define('forum/topic', [ $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () { const link = $(this); link.one('mouseleave', function () { + setTimeout(() => { + // If the mouse leaves the link and it's not on a tooltip, destroy the tooltip after a short delay + if (!cursorOnPreviewTooltip) { + destroyTooltip(); + } + }, 300); + + // if mouse leaves the link before the tooltip is rendered, clear the timeout if (timeoutId > 0) { clearTimeout(timeoutId); timeoutId = 0; @@ -352,7 +362,9 @@ define('forum/topic', [ const linkRect = link.offset(); const { top } = link.get(0).getBoundingClientRect(); const dropup = top > window.innerHeight / 2; - + tooltip.on('mouseenter', function () { + onPreviewTooltip = true; + }); tooltip.one('mouseleave', destroyTooltip); $(window).off('click', onClickOutside).one('click', onClickOutside); tooltip.css({ From 32faaba0e5a53da8c6ba4eb9192c6dd157a252ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 13:36:23 -0400 Subject: [PATCH 06/12] fix: more edge cases --- public/src/client/topic.js | 119 +++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 63 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 3bb50a5da5..43a64a9fa3 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -307,16 +307,16 @@ define('forum/topic', [ if (!ajaxify.data.showPostPreviewsOnHover || utils.isMobile()) { return; } - let timeoutId = 0; + let renderTimeout = 0; let destroyed = false; - let cursorOnPreviewTooltip = false; + let link = null; + const postCache = {}; function destroyTooltip() { - clearTimeout(timeoutId); - timeoutId = 0; + clearTimeout(renderTimeout); + renderTimeout = 0; $('#post-tooltip').remove(); destroyed = true; - cursorOnPreviewTooltip = false; } function onClickOutside(ev) { @@ -327,79 +327,72 @@ define('forum/topic', [ } $(window).one('action:ajaxify.start', destroyTooltip); + $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () { - const link = $(this); + link = $(this); + link.removeAttr('over-tooltip'); link.one('mouseleave', function () { + clearTimeout(renderTimeout); + renderTimeout = 0; setTimeout(() => { - // If the mouse leaves the link and it's not on a tooltip, destroy the tooltip after a short delay - if (!cursorOnPreviewTooltip) { + if (!link.attr('over-tooltip') && !renderTimeout) { destroyTooltip(); } - }, 300); - - // if mouse leaves the link before the tooltip is rendered, clear the timeout - if (timeoutId > 0) { - clearTimeout(timeoutId); - timeoutId = 0; - } + }, 100); }); + clearTimeout(renderTimeout); destroyed = false; - async function renderPost(pid) { - const postData = postCache[pid] || await api.get(`/posts/${encodeURIComponent(pid)}/summary`); - $('#post-tooltip').remove(); - if (postData && ajaxify.data.template.topic) { - postCache[pid] = postData; - const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData }); - if (destroyed) { - return; + renderTimeout = setTimeout(async () => { + async function renderPost(pid) { + const postData = postCache[pid] || await api.get(`/posts/${encodeURIComponent(pid)}/summary`); + $('#post-tooltip').remove(); + if (postData && ajaxify.data.template.topic) { + postCache[pid] = postData; + const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData }); + if (destroyed) { + return; + } + tooltip.hide().find('.timeago').timeago(); + tooltip.appendTo($('body')).fadeIn(300); + const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first(); + const postRect = postContent.offset(); + const postWidth = postContent.width(); + const linkRect = link.offset(); + const { top } = link.get(0).getBoundingClientRect(); + const dropup = top > window.innerHeight / 2; + tooltip.on('mouseenter', function () { + link.attr('over-tooltip', 1); + }); + tooltip.one('mouseleave', destroyTooltip); + $(window).off('click', onClickOutside).one('click', onClickOutside); + tooltip.css({ + top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, + left: postRect.left, + width: postWidth, + }); } - tooltip.hide().find('.timeago').timeago(); - tooltip.appendTo($('body')).fadeIn(300); - const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first(); - const postRect = postContent.offset(); - const postWidth = postContent.width(); - const linkRect = link.offset(); - const { top } = link.get(0).getBoundingClientRect(); - const dropup = top > window.innerHeight / 2; - tooltip.on('mouseenter', function () { - onPreviewTooltip = true; - }); - tooltip.one('mouseleave', destroyTooltip); - $(window).off('click', onClickOutside).one('click', onClickOutside); - tooltip.css({ - top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, - left: postRect.left, - width: postWidth, - }); - } - } - - const href = link.attr('href'); - const location = utils.urlToLocation(href); - const pathname = location.pathname; - const validHref = href && href !== '#' && window.location.hostname === location.hostname; - $('#post-tooltip').remove(); - const postMatch = validHref && pathname && pathname.match(/\/post\/([\d]+|(?:[\w_.~!$&'()*+,;=:@-]|%[\dA-F]{2})+)/); - const topicMatch = validHref && pathname && pathname.match(/\/topic\/([\da-z-]+)/); - if (postMatch) { - const pid = postMatch[1]; - if (encodeURIComponent(link.parents('[component="post"]').attr('data-pid')) === encodeURIComponent(pid)) { - return; // dont render self post } - timeoutId = setTimeout(async () => { - timeoutId = 0; + const href = link.attr('href'); + const location = utils.urlToLocation(href); + const pathname = location.pathname; + const validHref = href && href !== '#' && window.location.hostname === location.hostname; + $('#post-tooltip').remove(); + const postMatch = validHref && pathname && pathname.match(/\/post\/([\d]+|(?:[\w_.~!$&'()*+,;=:@-]|%[\dA-F]{2})+)/); + const topicMatch = validHref && pathname && pathname.match(/\/topic\/([\da-z-]+)/); + if (postMatch) { + const pid = postMatch[1]; + if (encodeURIComponent(link.parents('[component="post"]').attr('data-pid')) === encodeURIComponent(pid)) { + return; // dont render self post + } renderPost(pid); - }, 300); - } else if (topicMatch) { - timeoutId = setTimeout(async () => { - timeoutId = 0; + } else if (topicMatch) { const tid = topicMatch[1]; const topicData = await api.get('/topics/' + tid, {}); renderPost(topicData.mainPid); - }, 300); - } + } + }, 300); }); } From 6c5b22684bdb30598dd7166f8fd4104ab7bd177b Mon Sep 17 00:00:00 2001 From: cliffmccarthy <16453869+cliffmccarthy@users.noreply.github.com> Date: Wed, 11 Jun 2025 08:52:36 -0500 Subject: [PATCH 07/12] fix: Revise package hash check in Docker entrypoint.sh (#13483) - In the build_forum() function, the file install_hash.md5 is intended to track the content of package.json and detect changes that imply the need to run 'nodebb upgrade'. - The check to compare the current checksum of package.json to the one saved in install_hash.md5 is reversed. The "package.json was updated" branch is taken when the hashes are the same, not when they are different. - When install_hash.md5 does not exist, the comparison value becomes the null string, which never matches the checksum of package.json. As a result, the code always takes the "No changes in package.json" branch and returns from the function without creating install_hash.md5. As a result, install_hash.md5 never gets created on a new installation. - Revised build_forum() to use "not equals" when comparing the two checksums. This causes it to run 'nodebb upgrade' when the checksums are different, and also when install_hash.md5 does not yet exist. If the checksum saved in install_hash.md5 matches the current package.json checksum, it proceeds to either the "Build before start" case or the "No changes" case. --- install/docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/docker/entrypoint.sh b/install/docker/entrypoint.sh index dd17d707e7..db2a637ee0 100755 --- a/install/docker/entrypoint.sh +++ b/install/docker/entrypoint.sh @@ -103,7 +103,7 @@ build_forum() { local config="$1" local start_build="$2" local package_hash=$(md5sum install/package.json | head -c 32) - if [ "$package_hash" = "$(cat $CONFIG_DIR/install_hash.md5 || true)" ]; then + if [ "$package_hash" != "$(cat $CONFIG_DIR/install_hash.md5 || true)" ]; then echo "package.json was updated. Upgrading..." /usr/src/app/nodebb upgrade --config="$config" || { echo "Failed to build NodeBB. Exiting..." From 84d99a0fc775f01e1dc28b4c80ad78b58d539263 Mon Sep 17 00:00:00 2001 From: Eli Sheinfeld Date: Wed, 11 Jun 2025 20:13:23 +0300 Subject: [PATCH 08/12] feat: Add live reload functionality with Grunt watch and Socket.IO (#13489) - Added livereload event to Grunt watch tasks for instant browser refresh - Integrated Socket.IO WebSocket communication for real-time updates - Enhanced development workflow with immediate file change detection - Improved developer experience with automatic browser reload on file changes Changes: - Gruntfile.js: Send livereload message when files change - src/start.js: Handle livereload events and broadcast via Socket.IO --- Gruntfile.js | 2 ++ src/start.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index dcfa831cd6..53a4b7e06f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -174,6 +174,8 @@ module.exports = function (grunt) { } if (worker) { worker.send({ compiling: compiling }); + // Send livereload event via Socket.IO for instant browser refresh + worker.send({ livereload: true }); } }); }); diff --git a/src/start.js b/src/start.js index 99f3b662c5..a15aa44c6a 100644 --- a/src/start.js +++ b/src/start.js @@ -115,6 +115,13 @@ function addProcessHandlers() { const translator = require('./translator'); translator.flush(); } + } else if (msg && msg.livereload) { + // Send livereload event to all connected clients via Socket.IO + const websockets = require('./socket.io'); + if (websockets.server) { + websockets.server.emit('event:livereload'); + winston.info('[livereload] Sent reload event to all clients'); + } } }); } From dc37789b5dd4888332c6ef4c3ede65fedcdd2452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 11 Jun 2025 13:16:52 -0400 Subject: [PATCH 09/12] refactor: send single message --- Gruntfile.js | 7 ++++--- src/start.js | 30 +++++++++++++++++------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 53a4b7e06f..60d8f8b23e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -173,9 +173,10 @@ module.exports = function (grunt) { winston.error(err.stack); } if (worker) { - worker.send({ compiling: compiling }); - // Send livereload event via Socket.IO for instant browser refresh - worker.send({ livereload: true }); + worker.send({ + compiling: compiling, + livereload: true, // Send livereload event via Socket.IO for instant browser refresh + }); } }); }); diff --git a/src/start.js b/src/start.js index a15aa44c6a..c4dd925aad 100644 --- a/src/start.js +++ b/src/start.js @@ -107,20 +107,24 @@ function addProcessHandlers() { shutdown(1); }); process.on('message', (msg) => { - if (msg && Array.isArray(msg.compiling)) { - if (msg.compiling.includes('tpl')) { - const benchpressjs = require('benchpressjs'); - benchpressjs.flush(); - } else if (msg.compiling.includes('lang')) { - const translator = require('./translator'); - translator.flush(); + if (msg) { + if (Array.isArray(msg.compiling)) { + if (msg.compiling.includes('tpl')) { + const benchpressjs = require('benchpressjs'); + benchpressjs.flush(); + } else if (msg.compiling.includes('lang')) { + const translator = require('./translator'); + translator.flush(); + } } - } else if (msg && msg.livereload) { - // Send livereload event to all connected clients via Socket.IO - const websockets = require('./socket.io'); - if (websockets.server) { - websockets.server.emit('event:livereload'); - winston.info('[livereload] Sent reload event to all clients'); + + if (msg.livereload) { + // Send livereload event to all connected clients via Socket.IO + const websockets = require('./socket.io'); + if (websockets.server) { + websockets.server.emit('event:livereload'); + winston.info('[livereload] Sent reload event to all clients'); + } } } }); From da2597f81ce5c7df5e02a90d5215e2da087d1f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 11 Jun 2025 17:13:56 -0400 Subject: [PATCH 10/12] fix: sanitize svg when uploading site-logo, default avatar and og:image --- src/controllers/admin/uploads.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 56d64674cf..ccd4261b36 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -258,10 +258,6 @@ uploadsController.uploadMaskableIcon = async function (req, res, next) { } }; -uploadsController.uploadLogo = async function (req, res, next) { - await upload('site-logo', req, res, next); -}; - uploadsController.uploadFile = async function (req, res, next) { const uploadedFile = req.files.files[0]; let params; @@ -285,6 +281,10 @@ uploadsController.uploadFile = async function (req, res, next) { } }; +uploadsController.uploadLogo = async function (req, res, next) { + await upload('site-logo', req, res, next); +}; + uploadsController.uploadDefaultAvatar = async function (req, res, next) { await upload('avatar-default', req, res, next); }; @@ -296,6 +296,10 @@ uploadsController.uploadOgImage = async function (req, res, next) { async function upload(name, req, res, next) { const uploadedFile = req.files.files[0]; + if (uploadedFile.path.endsWith('.svg')) { + await sanitizeSvg(uploadedFile.path); + } + await validateUpload(uploadedFile, allowedImageTypes); const filename = name + path.extname(uploadedFile.name); await uploadImage(filename, 'system', uploadedFile, req, res, next); From 8c69c6a0c4399debac837469dbbb1a142d220679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Jun 2025 09:17:57 -0400 Subject: [PATCH 11/12] feat: link to post in preview timestamp --- src/views/partials/topic/post-preview.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/partials/topic/post-preview.tpl b/src/views/partials/topic/post-preview.tpl index c307e14f81..d3436bf6ba 100644 --- a/src/views/partials/topic/post-preview.tpl +++ b/src/views/partials/topic/post-preview.tpl @@ -6,7 +6,7 @@ {post.user.username}
- +
{post.content}
From a3fed408e571b4f4be0dfcf75539dd77f95ae60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Jun 2025 09:21:00 -0400 Subject: [PATCH 12/12] change default to perma ban --- src/views/modals/temporary-ban.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/modals/temporary-ban.tpl b/src/views/modals/temporary-ban.tpl index d84bc28331..33bd5c8b47 100644 --- a/src/views/modals/temporary-ban.tpl +++ b/src/views/modals/temporary-ban.tpl @@ -12,7 +12,7 @@
- +