From 658dd03b0390c7aca1475a59f915b432ba764ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 8 Jul 2020 14:09:10 -0400 Subject: [PATCH] feat: add tools to recent/unread (#8477) * feat: add tools to recent/unread * fix: open api spec * fix: more api spec --- public/openapi/read.yaml | 32 ++++++++++++ public/src/client/category.js | 12 +---- public/src/client/category/tools.js | 24 ++++----- public/src/modules/topicList.js | 6 ++- src/categories/delete.js | 3 ++ src/controllers/category.js | 1 + src/controllers/helpers.js | 4 ++ src/controllers/recent.js | 5 +- src/controllers/unread.js | 6 ++- src/socket.io/helpers.js | 5 +- src/socket.io/topics/move.js | 6 ++- src/socket.io/topics/tools.js | 9 +++- src/topics/create.js | 6 ++- src/topics/delete.js | 17 ++----- src/topics/recent.js | 4 +- .../1.14.1/readd_deleted_recent_topics.js | 49 +++++++++++++++++++ 16 files changed, 139 insertions(+), 50 deletions(-) create mode 100644 src/upgrades/1.14.1/readd_deleted_recent_topics.js diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 8d8dadd8e8..29ac388079 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -3810,6 +3810,10 @@ paths: type: number canPost: type: boolean + showSelect: + type: boolean + showTopicTools: + type: boolean categories: type: array items: @@ -3871,6 +3875,8 @@ paths: type: boolean filter: type: string + icon: + type: string selectedFilter: type: object properties: @@ -3882,6 +3888,8 @@ paths: type: boolean filter: type: string + icon: + type: string terms: type: array items: @@ -3946,6 +3954,8 @@ paths: properties: showSelect: type: boolean + showTopicTools: + type: boolean nextStart: type: number topics: @@ -4199,6 +4209,8 @@ paths: type: boolean filter: type: string + icon: + type: string selectedFilter: type: object properties: @@ -4210,6 +4222,8 @@ paths: type: boolean filter: type: string + icon: + type: string - $ref: components/schemas/Pagination.yaml#/Pagination - $ref: components/schemas/Breadcrumbs.yaml#/Breadcrumbs - $ref: components/schemas/CommonProps.yaml#/CommonProps @@ -5492,6 +5506,10 @@ paths: type: number canPost: type: boolean + showSelect: + type: boolean + showTopicTools: + type: boolean categories: type: array items: @@ -5553,6 +5571,8 @@ paths: type: boolean filter: type: string + icon: + type: string selectedFilter: type: object properties: @@ -5564,6 +5584,8 @@ paths: type: boolean filter: type: string + icon: + type: string terms: type: array items: @@ -5620,6 +5642,10 @@ paths: type: number canPost: type: boolean + showSelect: + type: boolean + showTopicTools: + type: boolean categories: type: array items: @@ -5694,6 +5720,8 @@ paths: type: boolean filter: type: string + icon: + type: string selectedFilter: type: object properties: @@ -5705,6 +5733,8 @@ paths: type: boolean filter: type: string + icon: + type: string terms: type: array items: @@ -5817,6 +5847,8 @@ paths: type: boolean showSelect: type: boolean + showTopicTools: + type: boolean rssFeedUrl: type: string feeds:disableRSS: diff --git a/public/src/client/category.js b/public/src/client/category.js index f02eeb7c40..070616071f 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -4,25 +4,17 @@ define('forum/category', [ 'forum/infinitescroll', 'share', 'navigator', - 'forum/category/tools', 'topicList', 'sort', -], function (infinitescroll, share, navigator, categoryTools, topicList, sort) { +], function (infinitescroll, share, navigator, topicList, sort) { var Category = {}; $(window).on('action:ajaxify.start', function (ev, data) { if (!String(data.url).startsWith('category/')) { navigator.disable(); - - removeListeners(); } }); - function removeListeners() { - categoryTools.removeListeners(); - topicList.removeListeners(); - } - Category.init = function () { var cid = ajaxify.data.cid; @@ -30,8 +22,6 @@ define('forum/category', [ share.addShareHandlers(ajaxify.data.name); - categoryTools.init(cid); - topicList.init('category', loadTopicsAfter); sort.handleSort('categoryTopicSort', 'user.setCategorySort', 'category/' + ajaxify.data.slug); diff --git a/public/src/client/category/tools.js b/public/src/client/category/tools.js index f219a399af..544ab31581 100644 --- a/public/src/client/category/tools.js +++ b/public/src/client/category/tools.js @@ -9,9 +9,7 @@ define('forum/category/tools', [ ], function (topicSelect, components, translator) { var CategoryTools = {}; - CategoryTools.init = function (cid) { - CategoryTools.cid = cid; - + CategoryTools.init = function () { topicSelect.init(updateDropdownOptions); handlePinnedTopicSort(); @@ -36,7 +34,7 @@ define('forum/category/tools', [ if (!tids.length) { return app.alertError('[[error:no-topics-selected]]'); } - socket.emit('topics.lock', { tids: tids, cid: CategoryTools.cid }, onCommandComplete); + socket.emit('topics.lock', { tids: tids }, onCommandComplete); return false; }); @@ -45,7 +43,7 @@ define('forum/category/tools', [ if (!tids.length) { return app.alertError('[[error:no-topics-selected]]'); } - socket.emit('topics.unlock', { tids: tids, cid: CategoryTools.cid }, onCommandComplete); + socket.emit('topics.unlock', { tids: tids }, onCommandComplete); return false; }); @@ -54,7 +52,7 @@ define('forum/category/tools', [ if (!tids.length) { return app.alertError('[[error:no-topics-selected]]'); } - socket.emit('topics.pin', { tids: tids, cid: CategoryTools.cid }, onCommandComplete); + socket.emit('topics.pin', { tids: tids }, onCommandComplete); return false; }); @@ -63,7 +61,7 @@ define('forum/category/tools', [ if (!tids.length) { return app.alertError('[[error:no-topics-selected]]'); } - socket.emit('topics.unpin', { tids: tids, cid: CategoryTools.cid }, onCommandComplete); + socket.emit('topics.unpin', { tids: tids }, onCommandComplete); return false; }); @@ -92,13 +90,17 @@ define('forum/category/tools', [ if (!tids.length) { return app.alertError('[[error:no-topics-selected]]'); } - move.init(tids, cid, onCommandComplete); + move.init(tids, null, onCommandComplete); }); return false; }); components.get('topic/move-all').on('click', function () { + var cid = ajaxify.data.cid; + if (!ajaxify.data.template.category) { + return app.alertError('[[error:invalid-data]]'); + } require(['forum/topic/move'], function (move) { move.init(null, cid, function (err) { if (err) { @@ -110,7 +112,7 @@ define('forum/category/tools', [ }); }); - $('.category').on('click', '[component="topic/merge"]', function () { + components.get('topic/merge').on('click', function () { require(['forum/topic/merge'], function (merge) { merge.init(); }); @@ -138,7 +140,7 @@ define('forum/category/tools', [ return; } - socket.emit('topics.' + command, { tids: tids, cid: CategoryTools.cid }, onDeletePurgeComplete); + socket.emit('topics.' + command, { tids: tids }, onDeletePurgeComplete); }); }); } @@ -259,7 +261,7 @@ define('forum/category/tools', [ return memo; }, 0); - if (!ajaxify.data.privileges.isAdminOrMod || numPinned < 2) { + if ((!app.user.isAdmin && !app.user.isMod) || numPinned < 2) { return; } diff --git a/public/src/modules/topicList.js b/public/src/modules/topicList.js index 553eb3a18b..f9260a5eff 100644 --- a/public/src/modules/topicList.js +++ b/public/src/modules/topicList.js @@ -5,7 +5,8 @@ define('topicList', [ 'handleBack', 'topicSelect', 'categorySearch', -], function (infinitescroll, handleBack, topicSelect, categorySearch) { + 'forum/category/tools', +], function (infinitescroll, handleBack, topicSelect, categorySearch, categoryTools) { var TopicList = {}; var templateName = ''; @@ -24,6 +25,7 @@ define('topicList', [ $(window).on('action:ajaxify.start', function () { TopicList.removeListeners(); + categoryTools.removeListeners(); }); TopicList.init = function (template, cb) { @@ -32,6 +34,8 @@ define('topicList', [ templateName = template; loadTopicsCallback = cb || loadTopicsAfter; + categoryTools.init(); + TopicList.watchForNewPosts(); TopicList.handleCategorySelection(); diff --git a/src/categories/delete.js b/src/categories/delete.js index 033aa3d52d..30c92ae441 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -32,6 +32,9 @@ module.exports = function (Categories) { 'cid:' + cid + ':tids', 'cid:' + cid + ':tids:pinned', 'cid:' + cid + ':tids:posts', + 'cid:' + cid + ':tids:votes', + 'cid:' + cid + ':tids:lastposttime', + 'cid:' + cid + ':recent_tids', 'cid:' + cid + ':pids', 'cid:' + cid + ':read_by_uid', 'cid:' + cid + ':uid:watch:state', diff --git a/src/controllers/category.js b/src/controllers/category.js index befc2621cb..907bdbc0fd 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -95,6 +95,7 @@ categoryController.get = async function (req, res, next) { categoryData.description = translator.escape(categoryData.description); categoryData.privileges = userPrivileges; categoryData.showSelect = userPrivileges.editable; + categoryData.showTopicTools = userPrivileges.editable; categoryData.rssFeedUrl = nconf.get('url') + '/category/' + categoryData.cid + '.rss'; if (parseInt(req.uid, 10)) { categories.markAsRead([cid], req.uid); diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 41108267d6..5f560038d7 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -72,21 +72,25 @@ helpers.buildFilters = function (url, filter, query) { url: url + helpers.buildQueryString(query.cid, '', query.term), selected: filter === '', filter: '', + icon: 'fa-book', }, { name: '[[unread:new-topics]]', url: url + helpers.buildQueryString(query.cid, 'new', query.term), selected: filter === 'new', filter: 'new', + icon: 'fa-clock-o', }, { name: '[[unread:watched-topics]]', url: url + helpers.buildQueryString(query.cid, 'watched', query.term), selected: filter === 'watched', filter: 'watched', + icon: 'fa-bell-o', }, { name: '[[unread:unreplied-topics]]', url: url + helpers.buildQueryString(query.cid, 'unreplied', query.term), selected: filter === 'unreplied', filter: 'unreplied', + icon: 'fa-reply', }]; }; diff --git a/src/controllers/recent.js b/src/controllers/recent.js index 8e7d6769aa..98fbd8bb49 100644 --- a/src/controllers/recent.js +++ b/src/controllers/recent.js @@ -37,11 +37,12 @@ recentController.getData = async function (req, url, sort) { states.push(categories.watchStates.ignoring); } - const [settings, categoryData, rssToken, canPost] = await Promise.all([ + const [settings, categoryData, rssToken, canPost, isPrivileged] = await Promise.all([ user.getSettings(req.uid), helpers.getCategoriesByStates(req.uid, cid, states), user.auth.getFeedToken(req.uid), canPostTopic(req.uid), + user.isPrivileged(req.uid), ]); const start = Math.max(0, (page - 1) * settings.topicsPerPage); @@ -60,6 +61,8 @@ recentController.getData = async function (req, url, sort) { }); data.canPost = canPost; + data.showSelect = isPrivileged; + data.showTopicTools = isPrivileged; data.categories = categoryData.categories; data.allCategoriesUrl = url + helpers.buildQueryString('', filter, ''); data.selectedCategory = categoryData.selectedCategory || null; diff --git a/src/controllers/unread.js b/src/controllers/unread.js index b11a147058..e2f110756e 100644 --- a/src/controllers/unread.js +++ b/src/controllers/unread.js @@ -22,9 +22,10 @@ unreadController.get = async function (req, res, next) { if (!filterData.filters[filter]) { return next(); } - const [watchedCategories, userSettings] = await Promise.all([ + const [watchedCategories, userSettings, isPrivileged] = await Promise.all([ getWatchedCategories(req.uid, cid, filter), user.getSettings(req.uid), + user.isPrivileged(req.uid), ]); const page = parseInt(req.query.page, 10) || 1; @@ -48,7 +49,8 @@ unreadController.get = async function (req, res, next) { req.query.page = Math.max(1, Math.min(data.pageCount, page)); return helpers.redirect(res, '/unread?' + querystring.stringify(req.query)); } - + data.showSelect = isPrivileged; + data.showTopicTools = isPrivileged; data.categories = watchedCategories.categories; data.allCategoriesUrl = 'unread' + helpers.buildQueryString('', filter, ''); data.selectedCategory = watchedCategories.selectedCategory; diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 8cbd7464c0..380da8858f 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -191,9 +191,8 @@ SocketHelpers.rescindUpvoteNotification = async function (pid, fromuid) { websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); }; -SocketHelpers.emitToTopicAndCategory = function (event, data) { - websockets.in('topic_' + data.tid).emit(event, data); - websockets.in('category_' + data.cid).emit(event, data); +SocketHelpers.emitToTopicAndCategory = async function (event, data, uids) { + uids.forEach(toUid => websockets.in('uid_' + toUid).emit(event, data)); }; require('../promisify')(SocketHelpers); diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js index 47ef260857..046ad83156 100644 --- a/src/socket.io/topics/move.js +++ b/src/socket.io/topics/move.js @@ -1,6 +1,7 @@ 'use strict'; const async = require('async'); +const user = require('../../user'); const topics = require('../../topics'); const categories = require('../../categories'); const privileges = require('../../privileges'); @@ -12,6 +13,8 @@ module.exports = function (SocketTopics) { throw new Error('[[error:invalid-data]]'); } + const uids = await user.getUidsFromSet('users:online', 0, -1); + await async.eachLimit(data.tids, 10, async function (tid) { const canMove = await privileges.topics.isAdminOrMod(tid, socket.uid); if (!canMove) { @@ -21,7 +24,8 @@ module.exports = function (SocketTopics) { data.uid = socket.uid; await topics.tools.move(tid, data); - socketHelpers.emitToTopicAndCategory('event:topic_moved', topicData); + const notifyUids = await privileges.categories.filterUids('topics:read', topicData.cid, uids); + socketHelpers.emitToTopicAndCategory('event:topic_moved', topicData, notifyUids); if (!topicData.deleted) { socketHelpers.sendNotificationToTopicOwner(tid, socket.uid, 'move', 'notifications:moved_your_topic'); } diff --git a/src/socket.io/topics/tools.js b/src/socket.io/topics/tools.js index 649887c367..1b176a83be 100644 --- a/src/socket.io/topics/tools.js +++ b/src/socket.io/topics/tools.js @@ -1,5 +1,6 @@ 'use strict'; +const user = require('../../user'); const topics = require('../../topics'); const events = require('../../events'); const privileges = require('../../privileges'); @@ -65,17 +66,21 @@ module.exports = function (SocketTopics) { throw new Error('[[error:no-privileges]]'); } - if (!data || !Array.isArray(data.tids) || !data.cid) { + if (!data || !Array.isArray(data.tids)) { throw new Error('[[error:invalid-tid]]'); } if (typeof topics.tools[action] !== 'function') { return; } + + const uids = await user.getUidsFromSet('users:online', 0, -1); + await Promise.all(data.tids.map(async function (tid) { const title = await topics.getTopicField(tid, 'title'); const data = await topics.tools[action](tid, socket.uid); - socketHelpers.emitToTopicAndCategory(event, data); + const notifyUids = await privileges.categories.filterUids('topics:read', data.cid, uids); + socketHelpers.emitToTopicAndCategory(event, data, notifyUids); await logTopicAction(action, socket, tid, title); })); }; diff --git a/src/topics/create.js b/src/topics/create.js index 6943e4204d..ad7902d6ce 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -48,7 +48,11 @@ module.exports = function (Topics) { 'cid:' + topicData.cid + ':tids', 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', ], timestamp, topicData.tid), - db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', 0, topicData.tid), + db.sortedSetsAdd([ + 'topics:views', 'topics:posts', 'topics:votes', + 'cid:' + topicData.cid + ':tids:votes', + 'cid:' + topicData.cid + ':tids:posts', + ], 0, topicData.tid), categories.updateRecentTid(topicData.cid, topicData.tid), user.addTopicIdToUser(topicData.uid, topicData.tid, timestamp), db.incrObjectField('category:' + topicData.cid, 'topic_count'), diff --git a/src/topics/delete.js b/src/topics/delete.js index f04d24e778..fce87776c2 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -18,12 +18,6 @@ module.exports = function (Topics) { deleterUid: uid, deletedTimestamp: Date.now(), }), - db.sortedSetsRemove([ - 'topics:recent', - 'topics:posts', - 'topics:views', - 'topics:votes', - ], tid), removeTopicPidsFromCid(tid), ]); }; @@ -55,16 +49,11 @@ module.exports = function (Topics) { } Topics.restore = async function (tid) { - const topicData = await Topics.getTopicData(tid); + await Topics.deleteTopicFields(tid, [ + 'deleterUid', 'deletedTimestamp', + ]); await Promise.all([ Topics.setTopicField(tid, 'deleted', 0), - Topics.deleteTopicFields(tid, ['deleterUid', 'deletedTimestamp']), - Topics.updateRecent(tid, topicData.lastposttime), - db.sortedSetAddBulk([ - ['topics:posts', topicData.postcount, tid], - ['topics:views', topicData.viewcount, tid], - ['topics:votes', parseInt(topicData.votes, 10) || 0, tid], - ]), addTopicPidsToCid(tid), ]); }; diff --git a/src/topics/recent.js b/src/topics/recent.js index 8f51c640e7..2a7f54fecd 100644 --- a/src/topics/recent.js +++ b/src/topics/recent.js @@ -60,9 +60,7 @@ module.exports = function (Topics) { await db.sortedSetAdd('cid:' + topicData.cid + ':tids:lastposttime', lastposttime, tid); - if (!topicData.deleted) { - await Topics.updateRecent(tid, lastposttime); - } + await Topics.updateRecent(tid, lastposttime); if (!topicData.pinned) { await db.sortedSetAdd('cid:' + topicData.cid + ':tids', lastposttime, tid); diff --git a/src/upgrades/1.14.1/readd_deleted_recent_topics.js b/src/upgrades/1.14.1/readd_deleted_recent_topics.js new file mode 100644 index 0000000000..14580953fb --- /dev/null +++ b/src/upgrades/1.14.1/readd_deleted_recent_topics.js @@ -0,0 +1,49 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); + +module.exports = { + name: 'Re add deleted topics to topics:recent', + timestamp: Date.UTC(2018, 9, 11), + method: async function () { + const progress = this.progress; + + await batch.processSortedSet('topics:tid', async function (tids) { + progress.incr(tids.length); + const topicData = await db.getObjectsFields( + tids.map(tid => 'topic:' + tid), + ['tid', 'lastposttime', 'viewcount', 'postcount', 'upvotes', 'downvotes'] + ); + topicData.forEach((t) => { + if (t.hasOwnProperty('upvotes') && t.hasOwnProperty('downvotes')) { + t.votes = parseInt(t.upvotes, 10) - parseInt(t.downvotes, 10); + } + }); + + await db.sortedSetAdd('topics:recent', + topicData.map(t => t.lastposttime), + topicData.map(t => t.tid) + ); + + await db.sortedSetAdd('topics:views', + topicData.map(t => t.viewcount || 0), + topicData.map(t => t.tid) + ); + + await db.sortedSetAdd('topics:posts', + topicData.map(t => t.postcount || 0), + topicData.map(t => t.tid) + ); + + await db.sortedSetAdd('topics:votes', + topicData.map(t => t.votes || 0), + topicData.map(t => t.tid) + ); + }, { + progress: progress, + batchSize: 500, + }); + }, +};