mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-28 10:19:50 +01:00
Merge commit 'a3fed408e571b4f4be0dfcf75539dd77f95ae60b' into v4.x
This commit is contained in:
30
CHANGELOG.md
30
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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
25
src/start.js
25
src/start.js
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user