Merge commit 'a3fed408e571b4f4be0dfcf75539dd77f95ae60b' into v4.x

This commit is contained in:
Misty Release Bot
2025-06-18 14:20:36 +00:00
8 changed files with 142 additions and 64 deletions

View File

@@ -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

View File

@@ -173,7 +173,10 @@ module.exports = function (grunt) {
winston.error(err.stack);
}
if (worker) {
worker.send({ compiling: compiling });
worker.send({
compiling: compiling,
livereload: true, // Send livereload event via Socket.IO for instant browser refresh
});
}
});
});

View File

@@ -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..."

View File

@@ -307,66 +307,93 @@ define('forum/topic', [
if (!ajaxify.data.showPostPreviewsOnHover || utils.isMobile()) {
return;
}
let timeoutId = 0;
let renderTimeout = 0;
let destroyed = false;
let link = null;
const postCache = {};
function destroyTooltip() {
clearTimeout(timeoutId);
clearTimeout(renderTimeout);
renderTimeout = 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 () {
const link = $(this);
$('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () {
link = $(this);
link.removeAttr('over-tooltip');
link.one('mouseleave', function () {
clearTimeout(renderTimeout);
renderTimeout = 0;
setTimeout(() => {
if (!link.attr('over-tooltip') && !renderTimeout) {
destroyTooltip();
}
}, 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();
tooltip.css({
top: 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 () => {
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 () => {
} else if (topicMatch) {
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);
}
}, 300);
});
}
function setupQuickReply() {

View File

@@ -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);

View File

@@ -107,13 +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();
}
}
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');
}
}
}
});

View File

@@ -12,7 +12,7 @@
<div class="col-5">
<div class="mb-3">
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
<input class="form-control" id="length" name="length" type="number" min="0" value="1" />
<input class="form-control" id="length" name="length" type="number" min="0" value="0" />
</div>
<div class="form-check form-check-inline">
<label class="form-check-label" for="unit-hours">[[admin/manage/users:temp-ban.hours]]</label>

View File

@@ -1,11 +1,14 @@
<div id="post-tooltip" class="card card-body shadow bg-body text-body z-1 position-absolute">
<div class="d-flex flex-column gap-2">
<div class="d-flex gap-1 align-items-center">
<a href="{{{ if post.user.userslug }}}{config.relative_path}/user/{post.user.userslug}{{{ else }}}#{{{ end }}}">
{buildAvatar(post.user, "24px", true, "", "user/picture")} {post.user.username}
</a>
<span class="timeago text-xs" title="{post.timestampISO}"></span>
<div class="d-flex gap-2 align-items-center">
<div>
<a href="{{{ if post.user.userslug }}}{config.relative_path}/user/{post.user.userslug}{{{ else }}}#{{{ end }}}">{buildAvatar(post.user, "20px", true, "", "user/picture")}</a>
<a href="{{{ if post.user.userslug }}}{config.relative_path}/user/{post.user.userslug}{{{ else }}}#{{{ end }}}">{post.user.username}</a>
</div>
<div>
<a href="{config.relative_path}/post/{post.pid}" class="timeago text-xs text-secondary lh-1" style="vertical-align: middle;" title="{post.timestampISO}"></a>
</div>
</div>
<div class="content">{post.content}</div>
<div class="content ghost-scrollbar" style="max-height: 300px; overflow-y:auto;">{post.content}</div>
</div>
</div>