diff --git a/CHANGELOG.md b/CHANGELOG.md
index e41c2eec9e..9dde4f436a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,57 @@
+#### v3.7.3 (2024-04-03)
+
+##### Chores
+
+* up ntfy (0058ca68)
+* incrementing version number - v3.7.2 (cc257e7e)
+* update changelog for v3.7.2 (277e1787)
+* incrementing version number - v3.7.1 (712365a5)
+* incrementing version number - v3.7.0 (9a6153d7)
+* incrementing version number - v3.6.7 (86a17e38)
+* incrementing version number - v3.6.6 (6604bf37)
+* incrementing version number - v3.6.5 (6c653625)
+* incrementing version number - v3.6.4 (83d131b4)
+* incrementing version number - v3.6.3 (fc7d2bfd)
+* incrementing version number - v3.6.2 (0f577a57)
+* incrementing version number - v3.6.1 (f1a69468)
+* incrementing version number - v3.6.0 (4cdf85f8)
+* incrementing version number - v3.5.3 (ed0e8783)
+* incrementing version number - v3.5.2 (52fbb2da)
+* incrementing version number - v3.5.1 (4c543488)
+* incrementing version number - v3.5.0 (d06fb4f0)
+* incrementing version number - v3.4.3 (5c984250)
+* incrementing version number - v3.4.2 (3f0dac38)
+* incrementing version number - v3.4.1 (01e69574)
+* incrementing version number - v3.4.0 (fd9247c5)
+* incrementing version number - v3.3.9 (5805e770)
+* incrementing version number - v3.3.8 (a5603565)
+* incrementing version number - v3.3.7 (b26f1744)
+* incrementing version number - v3.3.6 (7fb38792)
+* incrementing version number - v3.3.4 (a67f84ea)
+* incrementing version number - v3.3.3 (f94d239b)
+* incrementing version number - v3.3.2 (ec9dac97)
+* incrementing version number - v3.3.1 (151cc68f)
+* incrementing version number - v3.3.0 (fc1ad70f)
+* incrementing version number - v3.2.3 (b06d3e63)
+* incrementing version number - v3.2.2 (758ecfcd)
+* incrementing version number - v3.2.1 (20145074)
+* incrementing version number - v3.2.0 (9ecac38e)
+* incrementing version number - v3.1.7 (0b4e81ab)
+* incrementing version number - v3.1.6 (b3a3b130)
+* incrementing version number - v3.1.5 (ec19343a)
+* incrementing version number - v3.1.4 (2452783c)
+* incrementing version number - v3.1.3 (3b4e9d3f)
+* incrementing version number - v3.1.2 (40fa3489)
+* incrementing version number - v3.1.1 (40250733)
+* incrementing version number - v3.1.0 (0cb386bd)
+* incrementing version number - v3.0.1 (26f6ea49)
+* incrementing version number - v3.0.0 (224e08cd)
+
+##### Bug Fixes
+
+* change digest to use posts sorting first (3aae9234)
+* #12452, fix admin/mod image change (c206ccdd)
+
#### v3.7.2 (2024-03-27)
##### Chores
diff --git a/install/package.json b/install/package.json
index 2f31780b88..00a448b764 100644
--- a/install/package.json
+++ b/install/package.json
@@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
- "version": "3.7.2",
+ "version": "3.7.3",
"homepage": "https://www.nodebb.org",
"repository": {
"type": "git",
@@ -103,10 +103,10 @@
"nodebb-plugin-ntfy": "1.7.4",
"nodebb-plugin-spam-be-gone": "2.2.1",
"nodebb-rewards-essentials": "1.0.0",
- "nodebb-theme-harmony": "1.2.47",
+ "nodebb-theme-harmony": "1.2.48",
"nodebb-theme-lavender": "7.1.8",
"nodebb-theme-peace": "2.2.4",
- "nodebb-theme-persona": "13.3.13",
+ "nodebb-theme-persona": "13.3.14",
"nodebb-widget-essentials": "7.0.15",
"nodemailer": "6.9.13",
"nprogress": "0.2.0",
diff --git a/public/language/en-GB/admin/settings/navigation.json b/public/language/en-GB/admin/settings/navigation.json
index 931ac5f4ba..3a71061ecf 100644
--- a/public/language/en-GB/admin/settings/navigation.json
+++ b/public/language/en-GB/admin/settings/navigation.json
@@ -10,7 +10,7 @@
"id": "ID: optional",
"properties": "Properties:",
- "groups": "Groups:",
+ "show-to-groups": "Show to Groups:",
"open-new-window": "Open in a new window",
"dropdown": "Dropdown",
"dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a class="dropdown-item" href="https://myforum.com">Link 1</a></li>",
diff --git a/public/openapi/components/schemas/UserObject.yaml b/public/openapi/components/schemas/UserObject.yaml
index bdd52bc2d1..9b217cee8b 100644
--- a/public/openapi/components/schemas/UserObject.yaml
+++ b/public/openapi/components/schemas/UserObject.yaml
@@ -743,9 +743,7 @@ BanMuteArray:
example: "#f44336"
until:
type: number
- untilReadable:
- type: string
- timestampReadable:
+ untilISO:
type: string
timestampISO:
type: string
diff --git a/public/openapi/read/user/userslug/info.yaml b/public/openapi/read/user/userslug/info.yaml
index afec9bc2bd..110e252be5 100644
--- a/public/openapi/read/user/userslug/info.yaml
+++ b/public/openapi/read/user/userslug/info.yaml
@@ -35,8 +35,6 @@ get:
timestampISO:
type: string
description: An ISO 8601 formatted date string (complementing `timestamp`)
- timestampReadable:
- type: string
additionalProperties:
description: Contextual data is added to this object (such as topic data, etc.)
bans:
diff --git a/public/src/client/account/blocks.js b/public/src/client/account/blocks.js
index 921065fd37..91b0745a19 100644
--- a/public/src/client/account/blocks.js
+++ b/public/src/client/account/blocks.js
@@ -11,12 +11,19 @@ define('forum/account/blocks', [
Blocks.init = function () {
header.init();
const blockListEl = $('[component="blocks/search/list"]');
+ const startTypingEl = blockListEl.find('[component="blocks/start-typing"]');
+ const noUsersEl = blockListEl.find('[component="blocks/no-users"]');
- $('#user-search').on('keyup', function () {
+ $('#user-search').on('keyup', utils.debounce(function () {
const username = this.value;
+
if (!username) {
- return blockListEl.translateHtml('
[[admin/menu:search.start-typing]]');
+ blockListEl.find('[component="blocks/search/match"]').remove();
+ startTypingEl.removeClass('hidden');
+ noUsersEl.addClass('hidden');
+ return;
}
+ startTypingEl.addClass('hidden');
api.get('/api/users', {
query: username,
searchBy: 'username',
@@ -26,8 +33,10 @@ define('forum/account/blocks', [
return alerts.error(err);
}
if (!data.users.length) {
- return blockListEl.translateHtml('[[users:no-users-found]]');
+ noUsersEl.removeClass('hidden');
+ return;
}
+ noUsersEl.addClass('hidden');
// Only show first 10 matches
if (data.matchCount > 10) {
data.users.length = 10;
@@ -36,25 +45,36 @@ define('forum/account/blocks', [
app.parseAndTranslate('account/blocks', 'edit', {
edit: data.users,
}, function (html) {
- $('.block-edit').html(html);
+ blockListEl.find('[component="blocks/search/match"]').remove();
+ html.insertAfter(noUsersEl);
});
});
+ }, 200));
+
+ $('.block-edit').on('click', '[data-action="block"], [data-action="unblock"]', async function () {
+ const uid = parseInt(this.getAttribute('data-uid'), 10);
+ const action = $(this).attr('data-action');
+ const currentBtn = $(this);
+ await performBlock(uid, action);
+ currentBtn.addClass('hidden').siblings('[data-action]').removeClass('hidden');
+ Blocks.refreshList();
});
- $('.block-edit').on('click', '[data-action="toggle"]', function () {
- const uid = parseInt(this.getAttribute('data-uid'), 10);
- socket.emit('user.toggleBlock', {
- blockeeUid: uid,
- blockerUid: ajaxify.data.uid,
- }, Blocks.refreshList);
+ $('#users-container').on('click', '[data-action="unblock"]', async function () {
+ await performBlock($(this).attr('data-uid'), $(this).attr('data-action'));
+ Blocks.refreshList();
});
};
- Blocks.refreshList = function (err) {
- if (err) {
- return alerts.error(err);
- }
+ async function performBlock(uid, action) {
+ return socket.emit('user.toggleBlock', {
+ blockeeUid: uid,
+ blockerUid: ajaxify.data.uid,
+ action: action,
+ }).catch(alerts.error);
+ }
+ Blocks.refreshList = function () {
$.get(config.relative_path + '/api/' + ajaxify.currentPage)
.done(function (payload) {
app.parseAndTranslate('account/blocks', 'users', payload, function (html) {
diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js
index 9aae8d6a8f..dfa888120b 100644
--- a/public/src/client/account/header.js
+++ b/public/src/client/account/header.js
@@ -56,8 +56,8 @@ define('forum/account/header', [
components.get('account/delete-content').on('click', () => AccountsDelete.content(ajaxify.data.theirid));
components.get('account/delete-all').on('click', () => AccountsDelete.purge(ajaxify.data.theirid));
components.get('account/flag').on('click', flagAccount);
- components.get('account/block').on('click', toggleBlockAccount);
- components.get('account/unblock').on('click', toggleBlockAccount);
+ components.get('account/block').on('click', () => toggleBlockAccount('block'));
+ components.get('account/unblock').on('click', () => toggleBlockAccount('unblock'));
};
function selectActivePill() {
@@ -129,10 +129,11 @@ define('forum/account/header', [
});
}
- function toggleBlockAccount() {
+ function toggleBlockAccount(action) {
socket.emit('user.toggleBlock', {
blockeeUid: ajaxify.data.uid,
blockerUid: app.user.uid,
+ action,
}, function (err, blocked) {
if (err) {
return alerts.error(err);
diff --git a/public/src/client/chats.js b/public/src/client/chats.js
index a5cb58caf0..abaae9f818 100644
--- a/public/src/client/chats.js
+++ b/public/src/client/chats.js
@@ -225,18 +225,23 @@ define('forum/chats', [
Chats.addIPHandler = function (container) {
container.off('click', '.chat-ip-button')
- .on('click', '.chat-ip-button', async function () {
+ .on('click', '.chat-ip-button', async function (ev) {
+ ev.stopPropagation();
const ipEl = $(this);
+ const ipCopyText = ipEl.find('.copy .copy-ip-text');
let ip = ipEl.attr('data-ip');
if (ip) {
navigator.clipboard.writeText(ip);
- ipEl.translateText('[[global:copied]]');
- setTimeout(() => ipEl.text(ip), 2000);
+ ipCopyText.translateText('[[global:copied]]');
+ setTimeout(() => ipCopyText.text(ip), 2000);
return;
}
const mid = ipEl.parents('[data-mid]').attr('data-mid');
({ ip } = await api.get(`/chats/${ajaxify.data.roomId}/messages/${mid}/ip`));
- ipEl.text(ip).attr('data-ip', ip);
+ ipEl.attr('data-ip', ip);
+ ipEl.find('.show').addClass('hidden');
+ ipEl.find('.copy').removeClass('hidden');
+ ipCopyText.text(ip);
});
};
@@ -248,7 +253,8 @@ define('forum/chats', [
}
container.off('click', '[data-action="copy-link"]')
- .on('click', '[data-action="copy-link"]', function () {
+ .on('click', '[data-action="copy-link"]', function (ev) {
+ ev.stopPropagation();
const copyEl = $(this);
const mid = copyEl.attr('data-mid');
if (mid) {
@@ -257,7 +263,8 @@ define('forum/chats', [
});
container.off('click', '[data-action="copy-text"]')
- .on('click', '[data-action="copy-text"]', function () {
+ .on('click', '[data-action="copy-text"]', function (ev) {
+ ev.stopPropagation();
const copyEl = $(this);
const messageEl = copyEl.parents('[data-mid]');
if (messageEl.length) {
diff --git a/src/api/users.js b/src/api/users.js
index da9ea1a489..86cba2a193 100644
--- a/src/api/users.js
+++ b/src/api/users.js
@@ -600,6 +600,7 @@ usersAPI.search = async function (caller, data) {
throw new Error('[[error:no-privileges]]');
}
return await user.search({
+ uid: caller.uid,
query: data.query,
searchBy: data.searchBy || 'username',
page: data.page || 1,
diff --git a/src/controllers/users.js b/src/controllers/users.js
index f55296bdb9..41194e6c82 100644
--- a/src/controllers/users.js
+++ b/src/controllers/users.js
@@ -25,7 +25,7 @@ usersController.index = async function (req, res, next) {
if (req.query.query) {
await usersController.search(req, res, next);
- } else if (sectionToController[section]) {
+ } else if (sectionToController.hasOwnProperty(section) && sectionToController[section]) {
await sectionToController[section](req, res, next);
} else {
await usersController.getUsersSortedByJoinDate(req, res, next);
diff --git a/src/flags.js b/src/flags.js
index 833bb7edd8..e3932f3bd8 100644
--- a/src/flags.js
+++ b/src/flags.js
@@ -795,12 +795,10 @@ Flags.resolveUserPostFlags = async function (uid, callerUid) {
if (meta.config['flags:autoResolveOnBan']) {
await batch.processSortedSet(`uid:${uid}:posts`, async (pids) => {
let postData = await posts.getPostsFields(pids, ['pid', 'flagId']);
- postData = postData.filter(p => p && p.flagId);
+ postData = postData.filter(p => p && p.flagId && parseInt(p.flagId, 10));
for (const postObj of postData) {
- if (parseInt(postObj.flagId, 10)) {
- // eslint-disable-next-line no-await-in-loop
- await Flags.update(postObj.flagId, callerUid, { state: 'resolved' });
- }
+ // eslint-disable-next-line no-await-in-loop
+ await Flags.update(postObj.flagId, callerUid, { state: 'resolved' });
}
}, {
batch: 500,
diff --git a/src/middleware/render.js b/src/middleware/render.js
index 6b1181d3af..21ff25170d 100644
--- a/src/middleware/render.js
+++ b/src/middleware/render.js
@@ -214,7 +214,7 @@ module.exports = function (middleware) {
templateValues.isAdmin = results.user.isAdmin;
templateValues.isGlobalMod = results.user.isGlobalMod;
templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod;
- templateValues.canChat = results.privileges.chat && meta.config.disableChat !== 1;
+ templateValues.canChat = (results.privileges.chat || results.privileges['chat:privileged']) && meta.config.disableChat !== 1;
templateValues.user = results.user;
templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true });
templateValues.useCustomCSS = meta.config.useCustomCSS && meta.config.customCSS;
diff --git a/src/posts/delete.js b/src/posts/delete.js
index 66c8269334..94f73cf494 100644
--- a/src/posts/delete.js
+++ b/src/posts/delete.js
@@ -236,7 +236,7 @@ module.exports = function (Posts) {
}
async function resolveFlags(postData, uid) {
- const flaggedPosts = postData.filter(p => parseInt(p.flagId, 10));
+ const flaggedPosts = postData.filter(p => p && parseInt(p.flagId, 10));
await Promise.all(flaggedPosts.map(p => flags.update(p.flagId, uid, { state: 'resolved' })));
}
};
diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js
index 5d4c4624f3..277b75ccc4 100644
--- a/src/socket.io/user/profile.js
+++ b/src/socket.io/user/profile.js
@@ -41,8 +41,16 @@ module.exports = function (SocketUser) {
SocketUser.toggleBlock = async function (socket, data) {
const isBlocked = await user.blocks.is(data.blockeeUid, data.blockerUid);
- await user.blocks.can(socket.uid, data.blockerUid, data.blockeeUid, isBlocked ? 'unblock' : 'block');
- await user.blocks[isBlocked ? 'remove' : 'add'](data.blockeeUid, data.blockerUid);
+ const { action, blockerUid, blockeeUid } = data;
+ if (action !== 'block' && action !== 'unblock') {
+ throw new Error('[[error:unknow-block-action]]');
+ }
+ await user.blocks.can(socket.uid, blockerUid, blockeeUid, action);
+ if (data.action === 'block') {
+ await user.blocks.add(blockeeUid, blockerUid);
+ } else if (data.action === 'unblock') {
+ await user.blocks.remove(blockeeUid, blockerUid);
+ }
return !isBlocked;
};
};
diff --git a/src/topics/delete.js b/src/topics/delete.js
index 5190afd1ff..4e7f5d1400 100644
--- a/src/topics/delete.js
+++ b/src/topics/delete.js
@@ -5,25 +5,38 @@ const db = require('../database');
const user = require('../user');
const posts = require('../posts');
const categories = require('../categories');
+const flags = require('../flags');
const plugins = require('../plugins');
const batch = require('../batch');
module.exports = function (Topics) {
Topics.delete = async function (tid, uid) {
- const cid = await Topics.getTopicField(tid, 'cid');
- await removeTopicPidsFromCid(tid, cid);
- await Topics.setTopicFields(tid, {
- deleted: 1,
- deleterUid: uid,
- deletedTimestamp: Date.now(),
- });
+ const [cid, pids] = await Promise.all([
+ Topics.getTopicField(tid, 'cid'),
+ Topics.getPids(tid),
+ ]);
+ await Promise.all([
+ db.sortedSetRemove(`cid:${cid}:pids`, pids),
+ resolveTopicPostFlags(pids, uid),
+ Topics.setTopicFields(tid, {
+ deleted: 1,
+ deleterUid: uid,
+ deletedTimestamp: Date.now(),
+ }),
+ ]);
+
await categories.updateRecentTidForCid(cid);
};
- async function removeTopicPidsFromCid(tid, cid) {
- const pids = await Topics.getPids(tid);
- await db.sortedSetRemove(`cid:${cid}:pids`, pids);
+ async function resolveTopicPostFlags(pids, uid) {
+ await batch.processArray(pids, async (pids) => {
+ const postData = await posts.getPostsFields(pids, ['pid', 'flagId']);
+ const flaggedPosts = postData.filter(p => p && parseInt(p.flagId, 10));
+ await Promise.all(flaggedPosts.map(p => flags.update(p.flagId, uid, { state: 'resolved' })));
+ }, {
+ batch: 500,
+ });
}
async function addTopicPidsToCid(tid, cid) {
diff --git a/src/user/digest.js b/src/user/digest.js
index b07f54d1c3..61f4b2f12f 100644
--- a/src/user/digest.js
+++ b/src/user/digest.js
@@ -183,20 +183,23 @@ async function getTermTopics(term, uid) {
start: 0,
stop: 199,
term: term,
- sort: 'votes',
+ sort: 'posts',
teaserPost: 'first',
});
data.topics = data.topics.filter(topic => topic && !topic.deleted);
- const top = data.topics.filter(t => t.votes > 0).slice(0, 10);
- const topTids = top.map(t => t.tid);
-
const popular = data.topics
- .filter(t => t.postcount > 1 && !topTids.includes(t.tid))
+ .filter(t => t.postcount > 1)
.sort((a, b) => b.postcount - a.postcount)
.slice(0, 10);
const popularTids = popular.map(t => t.tid);
+ const top = data.topics
+ .filter(t => t.votes > 0 && !popularTids.includes(t.tid))
+ .sort((a, b) => b.votes - a.votes)
+ .slice(0, 10);
+ const topTids = top.map(t => t.tid);
+
const recent = data.topics
.filter(t => !topTids.includes(t.tid) && !popularTids.includes(t.tid))
.sort((a, b) => b.lastposttime - a.lastposttime)
diff --git a/src/user/info.js b/src/user/info.js
index 3abd580d02..d4667bd83f 100644
--- a/src/user/info.js
+++ b/src/user/info.js
@@ -86,7 +86,6 @@ module.exports = function (User) {
flagObj.pid = flagObj.value;
flagObj.timestamp = flagObj.score;
flagObj.timestampISO = new Date(flagObj.score).toISOString();
- flagObj.timestampReadable = new Date(flagObj.score).toString();
delete flagObj.value;
delete flagObj.score;
@@ -105,8 +104,7 @@ module.exports = function (User) {
return data.map((banObj, index) => {
banObj.user = usersData[index];
banObj.until = parseInt(banObj.expire, 10);
- banObj.untilReadable = new Date(banObj.until).toString();
- banObj.timestampReadable = new Date(parseInt(banObj.timestamp, 10)).toString();
+ banObj.untilISO = utils.toISOString(banObj.until);
banObj.timestampISO = utils.toISOString(banObj.timestamp);
banObj.reason = validator.escape(String(banObj.reason || '')) || noReasonLangKey;
return banObj;
diff --git a/src/user/search.js b/src/user/search.js
index 2713b3a8dd..ec0b81d025 100644
--- a/src/user/search.js
+++ b/src/user/search.js
@@ -60,7 +60,19 @@ module.exports = function (User) {
uids = uids.slice(start, stop);
}
- const userData = await User.getUsers(uids, uid);
+ const [userData, blocks] = await Promise.all([
+ User.getUsers(uids, uid),
+ User.blocks.list(uid),
+ ]);
+
+ if (blocks.length) {
+ userData.forEach((user) => {
+ if (user) {
+ user.isBlocked = blocks.includes(user.uid);
+ }
+ });
+ }
+
searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2);
searchResult.users = userData.filter(user => user && user.uid > 0);
return searchResult;
diff --git a/src/views/admin/settings/navigation.tpl b/src/views/admin/settings/navigation.tpl
index 431b052361..6af7e60511 100644
--- a/src/views/admin/settings/navigation.tpl
+++ b/src/views/admin/settings/navigation.tpl
@@ -75,7 +75,7 @@
-
+