From 2a9b0a3c9ca5d061864d75391b30e0044a60884e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 4 Mar 2024 16:06:04 -0500 Subject: [PATCH] feat: add new sorting option to categories add new zset for category topics fix sorting names --- install/package.json | 4 +- .../language/en-GB/admin/settings/post.json | 3 ++ public/language/en-GB/topic.json | 2 + src/api/categories.js | 2 +- src/categories/topics.js | 29 +++++++-------- src/controllers/api.js | 2 +- src/controllers/category.js | 4 +- src/topics/delete.js | 1 + src/topics/scheduled.js | 2 + src/topics/suggested.js | 2 +- src/topics/tools.js | 4 ++ .../3.7.0/category-tid-created-zset.js | 31 ++++++++++++++++ .../3.7.0/change-category-sort-settings.js | 37 +++++++++++++++++++ src/user/settings.js | 2 +- src/views/admin/settings/post.tpl | 6 ++- 15 files changed, 107 insertions(+), 24 deletions(-) create mode 100644 src/upgrades/3.7.0/category-tid-created-zset.js create mode 100644 src/upgrades/3.7.0/change-category-sort-settings.js diff --git a/install/package.json b/install/package.json index 1a31851c3e..73e813fa7f 100644 --- a/install/package.json +++ b/install/package.json @@ -103,10 +103,10 @@ "nodebb-plugin-ntfy": "1.7.3", "nodebb-plugin-spam-be-gone": "2.2.1", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.36", + "nodebb-theme-harmony": "1.2.37", "nodebb-theme-lavender": "7.1.7", "nodebb-theme-peace": "2.2.4", - "nodebb-theme-persona": "13.3.10", + "nodebb-theme-persona": "13.3.11", "nodebb-widget-essentials": "7.0.15", "nodemailer": "6.9.11", "nprogress": "0.2.0", diff --git a/public/language/en-GB/admin/settings/post.json b/public/language/en-GB/admin/settings/post.json index c93c901455..e000f6b10b 100644 --- a/public/language/en-GB/admin/settings/post.json +++ b/public/language/en-GB/admin/settings/post.json @@ -4,8 +4,11 @@ "sorting.post-default": "Default Post Sorting", "sorting.oldest-to-newest": "Oldest to Newest", "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.recently-replied": "Recently Replied", + "sorting.recently-created": "Recently Created", "sorting.most-votes": "Most Votes", "sorting.most-posts": "Most Posts", + "sorting.most-views": "Most Views", "sorting.topic-default": "Default Topic Sorting", "length": "Post Length", "post-queue": "Post Queue", diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index 78686a38cb..9027774603 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -206,6 +206,8 @@ "sort-by": "Sort by", "oldest-to-newest": "Oldest to Newest", "newest-to-oldest": "Newest to Oldest", + "recently-replied": "Recently Replied", + "recently-created": "Recently Created", "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", diff --git a/src/api/categories.js b/src/api/categories.js index c825d4fa2e..9a43a81526 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -124,7 +124,7 @@ categoriesAPI.getTopics = async (caller, data) => { } const infScrollTopicsPerPage = 20; - const sort = data.sort || data.categoryTopicSort || meta.config.categoryTopicSort || 'newest_to_oldest'; + const sort = data.sort || data.categoryTopicSort || meta.config.categoryTopicSort || 'recently_replied'; let start = Math.max(0, parseInt(data.after || 0, 10)); diff --git a/src/categories/topics.js b/src/categories/topics.js index 67695ec4c0..1a2a259f3d 100644 --- a/src/categories/topics.js +++ b/src/categories/topics.js @@ -27,10 +27,9 @@ module.exports = function (Categories) { }; Categories.getTopicIds = async function (data) { - const [pinnedTids, set, direction] = await Promise.all([ + const [pinnedTids, set] = await Promise.all([ Categories.getPinnedTids({ ...data, start: 0, stop: -1 }), Categories.buildTopicsSortedSet(data), - Categories.getSortedSetRangeDirection(data.sort), ]); const totalPinnedCount = pinnedTids.length; @@ -62,12 +61,11 @@ module.exports = function (Categories) { const stop = data.stop === -1 ? data.stop : start + normalTidsToGet - 1; let normalTids; - const reverse = direction === 'highest-to-lowest'; if (Array.isArray(set)) { const weights = set.map((s, index) => (index ? 0 : 1)); - normalTids = await db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ sets: set, start: start, stop: stop, weights: weights }); + normalTids = await db.getSortedSetRevIntersect({ sets: set, start: start, stop: stop, weights: weights }); } else { - normalTids = await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop); + normalTids = await db.getSortedSetRevRange(set, start, stop); } normalTids = normalTids.filter(tid => !pinnedTids.includes(tid)); return pinnedTidsOnPage.concat(normalTids); @@ -92,16 +90,16 @@ module.exports = function (Categories) { Categories.buildTopicsSortedSet = async function (data) { const { cid } = data; - let set = `cid:${cid}:tids`; - const sort = data.sort || (data.settings && data.settings.categoryTopicSort) || meta.config.categoryTopicSort || 'newest_to_oldest'; + const sort = data.sort || (data.settings && data.settings.categoryTopicSort) || meta.config.categoryTopicSort || 'recently_replied'; + const sortToSet = { + recently_replied: `cid:${cid}:tids`, + recently_created: `cid:${cid}:tids:create`, + most_posts: `cid:${cid}:tids:posts`, + most_votes: `cid:${cid}:tids:votes`, + most_views: `cid:${cid}:tids:views`, + }; - if (sort === 'most_posts') { - set = `cid:${cid}:tids:posts`; - } else if (sort === 'most_votes') { - set = `cid:${cid}:tids:votes`; - } else if (sort === 'most_views') { - set = `cid:${cid}:tids:views`; - } + let set = sortToSet.hasOwnProperty(sort) ? sortToSet[sort] : `cid:${cid}:tids`; if (data.tag) { if (Array.isArray(data.tag)) { @@ -123,7 +121,8 @@ module.exports = function (Categories) { }; Categories.getSortedSetRangeDirection = async function (sort) { - sort = sort || 'newest_to_oldest'; + console.warn('[deprecated] Will be removed in 4.x'); + sort = sort || 'recently_replied'; const direction = ['newest_to_oldest', 'most_posts', 'most_votes', 'most_views'].includes(sort) ? 'highest-to-lowest' : 'lowest-to-highest'; const result = await plugins.hooks.fire('filter:categories.getSortedSetRangeDirection', { sort: sort, diff --git a/src/controllers/api.js b/src/controllers/api.js index 30be616d8e..22574a9ce6 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -69,7 +69,7 @@ apiController.loadConfig = async function (req) { uid: req.uid, 'cache-buster': meta.config['cache-buster'] || '', topicPostSort: meta.config.topicPostSort || 'oldest_to_newest', - categoryTopicSort: meta.config.categoryTopicSort || 'newest_to_oldest', + categoryTopicSort: meta.config.categoryTopicSort || 'recently_replied', csrf_token: req.uid >= 0 ? generateToken(req) : false, searchEnabled: plugins.hooks.hasListeners('filter:search.query'), searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles', diff --git a/src/controllers/category.js b/src/controllers/category.js index 2f0f7d85ad..487ea21cce 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -20,7 +20,9 @@ const categoryController = module.exports; const url = nconf.get('url'); const relative_path = nconf.get('relative_path'); -const validSorts = ['newest_to_oldest', 'oldest_to_newest', 'most_posts', 'most_votes', 'most_views']; +const validSorts = [ + 'recently_replied', 'recently_created', 'most_posts', 'most_votes', 'most_views', +]; categoryController.get = async function (req, res, next) { const cid = req.params.category_id; diff --git a/src/topics/delete.js b/src/topics/delete.js index 75472ffc69..5190afd1ff 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -110,6 +110,7 @@ module.exports = function (Topics) { db.sortedSetsRemove([ `cid:${topicData.cid}:tids`, `cid:${topicData.cid}:tids:pinned`, + `cid:${topicData.cid}:tids:create`, `cid:${topicData.cid}:tids:posts`, `cid:${topicData.cid}:tids:lastposttime`, `cid:${topicData.cid}:tids:votes`, diff --git a/src/topics/scheduled.js b/src/topics/scheduled.js index 52d70366dc..0a91067d59 100644 --- a/src/topics/scheduled.js +++ b/src/topics/scheduled.js @@ -58,6 +58,7 @@ Scheduled.pin = async function (tid, topicData) { db.sortedSetAdd(`cid:${topicData.cid}:tids:pinned`, Date.now(), tid), db.sortedSetsRemove([ `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:tids:create`, `cid:${topicData.cid}:tids:posts`, `cid:${topicData.cid}:tids:votes`, `cid:${topicData.cid}:tids:views`, @@ -96,6 +97,7 @@ function unpin(tid, topicData) { db.sortedSetRemove(`cid:${topicData.cid}:tids:pinned`, tid), db.sortedSetAddBulk([ [`cid:${topicData.cid}:tids`, topicData.lastposttime, tid], + [`cid:${topicData.cid}:tids:create`, topicData.timestamp, tid], [`cid:${topicData.cid}:tids:posts`, topicData.postcount, tid], [`cid:${topicData.cid}:tids:votes`, parseInt(topicData.votes, 10) || 0, tid], [`cid:${topicData.cid}:tids:views`, topicData.viewcount, tid], diff --git a/src/topics/suggested.js b/src/topics/suggested.js index db59759a87..2d6f7db99c 100644 --- a/src/topics/suggested.js +++ b/src/topics/suggested.js @@ -65,7 +65,7 @@ module.exports = function (Topics) { const cid = await Topics.getTopicField(tid, 'cid'); const tids = cutoff === 0 ? await db.getSortedSetRevRange(`cid:${cid}:tids:lastposttime`, 0, 9) : - await db.getSortedSetRevRangeByScore(`cid:${cid}:tids:lastposttime`, 0, 9, '+inf', Date.now() - cutoff); + await db.getSortedSetRevRangeByScore(`cid:${cid}:tids:lastposttime`, 0, 10, '+inf', Date.now() - cutoff); return _.shuffle(tids.map(Number).filter(_tid => _tid !== tid)); } }; diff --git a/src/topics/tools.js b/src/topics/tools.js index be38f97209..cadeb95563 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -171,6 +171,7 @@ module.exports = function (Topics) { promises.push(db.sortedSetAdd(`cid:${topicData.cid}:tids:pinned`, Date.now(), tid)); promises.push(db.sortedSetsRemove([ `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:tids:create`, `cid:${topicData.cid}:tids:posts`, `cid:${topicData.cid}:tids:votes`, `cid:${topicData.cid}:tids:views`, @@ -180,6 +181,7 @@ module.exports = function (Topics) { promises.push(Topics.deleteTopicField(tid, 'pinExpiry')); promises.push(db.sortedSetAddBulk([ [`cid:${topicData.cid}:tids`, topicData.lastposttime, tid], + [`cid:${topicData.cid}:tids:create`, topicData.timestamp, tid], [`cid:${topicData.cid}:tids:posts`, topicData.postcount, tid], [`cid:${topicData.cid}:tids:votes`, parseInt(topicData.votes, 10) || 0, tid], [`cid:${topicData.cid}:tids:views`, topicData.viewcount, tid], @@ -242,6 +244,7 @@ module.exports = function (Topics) { const tags = await Topics.getTopicTags(tid); await db.sortedSetsRemove([ `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:tids:create`, `cid:${topicData.cid}:tids:pinned`, `cid:${topicData.cid}:tids:posts`, `cid:${topicData.cid}:tids:votes`, @@ -264,6 +267,7 @@ module.exports = function (Topics) { bulk.push([`cid:${cid}:tids:pinned`, Date.now(), tid]); } else { bulk.push([`cid:${cid}:tids`, topicData.lastposttime, tid]); + bulk.push([`cid:${cid}:tids:create`, topicData.timestamp, tid]); bulk.push([`cid:${cid}:tids:posts`, topicData.postcount, tid]); bulk.push([`cid:${cid}:tids:votes`, votes, tid]); bulk.push([`cid:${cid}:tids:views`, topicData.viewcount, tid]); diff --git a/src/upgrades/3.7.0/category-tid-created-zset.js b/src/upgrades/3.7.0/category-tid-created-zset.js new file mode 100644 index 0000000000..b7cb483a95 --- /dev/null +++ b/src/upgrades/3.7.0/category-tid-created-zset.js @@ -0,0 +1,31 @@ +'use strict'; + + +const db = require('../../database'); + +module.exports = { + name: 'New sorted set cid::tids:create', + timestamp: Date.UTC(2024, 2, 4), + method: async function () { + const { progress } = this; + const batch = require('../../batch'); + await batch.processSortedSet('topics:tid', async (tids) => { + let topicData = await db.getObjectsFields( + tids.map(tid => `topic:${tid}`), + ['tid', 'cid', 'timestamp'] + ); + topicData = topicData.filter(Boolean); + topicData.forEach((t) => { + t.timestamp = t.timestamp || Date.now(); + }); + + await db.sortedSetAddBulk( + topicData.map(t => ([`cid:${t.cid}:tids:create`, t.timestamp, t.tid])) + ); + + progress.incr(tids.length); + }, { + progress: this.progress, + }); + }, +}; diff --git a/src/upgrades/3.7.0/change-category-sort-settings.js b/src/upgrades/3.7.0/change-category-sort-settings.js new file mode 100644 index 0000000000..a5095dc775 --- /dev/null +++ b/src/upgrades/3.7.0/change-category-sort-settings.js @@ -0,0 +1,37 @@ +'use strict'; + + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Change category sort settings', + timestamp: Date.UTC(2024, 2, 4), + method: async function () { + const { progress } = this; + + const currentSort = await db.getObjectField('config', 'categoryTopicSort'); + if (currentSort === 'oldest_to_newest' || currentSort === 'newest_to_oldest') { + await db.setObjectField('config', 'categoryTopicSort', 'recently_replied'); + } + + await batch.processSortedSet('users:joindate', async (uids) => { + progress.incr(uids.length); + const usersSettings = await db.getObjects(uids.map(uid => `user:${uid}:settings`)); + const bulkSet = []; + usersSettings.forEach((userSetting, i) => { + if (userSetting && ( + userSetting.categoryTopicSort === 'newest_to_oldest' || + userSetting.categoryTopicSort === 'oldest_to_newest')) { + bulkSet.push([ + `user:${uids[i]}:settings`, { categoryTopicSort: 'recently_replied' }, + ]); + } + }); + await db.setObjectBulk(bulkSet); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/user/settings.js b/src/user/settings.js index 9d8c92fdc3..d85a712ba6 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -67,7 +67,7 @@ module.exports = function (User) { settings.userLang = settings.userLang || meta.config.defaultLang || 'en-GB'; settings.acpLang = settings.acpLang || settings.userLang; settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest'); - settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest'); + settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'recently_replied'); settings.followTopicsOnCreate = parseInt(getSetting(settings, 'followTopicsOnCreate', 1), 10) === 1; settings.followTopicsOnReply = parseInt(getSetting(settings, 'followTopicsOnReply', 0), 10) === 1; settings.upvoteNotifFreq = getSetting(settings, 'upvoteNotifFreq', 'all'); diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index bd99f75a4a..d361597946 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -18,9 +18,11 @@