diff --git a/public/src/client/account/categories.js b/public/src/client/account/categories.js index 8e162db809..bb6849b166 100644 --- a/public/src/client/account/categories.js +++ b/public/src/client/account/categories.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/account/categories', ['forum/account/header', 'alerts'], function (header, alerts) { +define('forum/account/categories', ['forum/account/header', 'alerts', 'api'], function (header, alerts, api) { const Categories = {}; Categories.init = function () { @@ -11,36 +11,32 @@ define('forum/account/categories', ['forum/account/header', 'alerts'], function handleIgnoreWatch(category.cid); }); - $('[component="category/watch/all"]').find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { + $('[component="category/watch/all"]').find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', async (e) => { const cids = []; - const state = $(this).attr('data-state'); + const state = e.currentTarget.getAttribute('data-state'); + const { uid } = ajaxify.data; $('[data-parent-cid="0"]').each(function (index, el) { cids.push($(el).attr('data-cid')); }); - socket.emit('categories.setWatchState', { cid: cids, state: state, uid: ajaxify.data.uid }, function (err, modified_cids) { - if (err) { - return alerts.error(err); - } - updateDropdowns(modified_cids, state); - }); + let modified_cids = await Promise.all(cids.map(async cid => api.put(`/categories/${cid}/watch`, { state, uid }))); + modified_cids = modified_cids + .reduce((memo, cur) => memo.concat(cur.modified), []) + .filter((cid, idx, arr) => arr.indexOf(cid) === idx); + + updateDropdowns(modified_cids, state); }); }; function handleIgnoreWatch(cid) { const category = $('[data-cid="' + cid + '"]'); - category.find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { - const $this = $(this); - const state = $this.attr('data-state'); + category.find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', async (e) => { + const state = e.currentTarget.getAttribute('data-state'); + const { uid } = ajaxify.data; - socket.emit('categories.setWatchState', { cid: cid, state: state, uid: ajaxify.data.uid }, function (err, modified_cids) { - if (err) { - return alerts.error(err); - } - updateDropdowns(modified_cids, state); - - alerts.success('[[category:' + state + '.message]]'); - }); + const { modified } = await api.put(`/categories/${cid}/watch`, { state, uid }); + updateDropdowns(modified, state); + alerts.success('[[category:' + state + '.message]]'); }); } diff --git a/public/src/client/category.js b/public/src/client/category.js index a89620cb34..3aff45420e 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -69,7 +69,7 @@ define('forum/category', [ const $this = $(this); const state = $this.attr('data-state'); - socket.emit('categories.setWatchState', { cid: cid, state: state }, function (err) { + api.put(`/categories/${cid}/watch`, { state }, (err) => { if (err) { return alerts.error(err); } diff --git a/src/api/categories.js b/src/api/categories.js index 28b2539a3a..442131b4fd 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -1,6 +1,7 @@ 'use strict'; const categories = require('../categories'); +const topics = require('../topics'); const events = require('../events'); const user = require('../user'); const groups = require('../groups'); @@ -84,6 +85,31 @@ categoriesAPI.getTopicCount = async (caller, { cid }) => { categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4); +categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => { + let targetUid = caller.uid; + const cids = Array.isArray(cid) ? cid.map(cid => parseInt(cid, 10)) : [parseInt(cid, 10)]; + if (uid) { + targetUid = uid; + } + await user.isAdminOrGlobalModOrSelf(caller.uid, targetUid); + const allCids = await categories.getAllCidsFromSet('categories:cid'); + const categoryData = await categories.getCategoriesFields(allCids, ['cid', 'parentCid']); + + // filter to subcategories of cid + let cat; + do { + cat = categoryData.find(c => !cids.includes(c.cid) && cids.includes(c.parentCid)); + if (cat) { + cids.push(cat.cid); + } + } while (cat); + + await user.setCategoryWatchState(targetUid, cids, state); + await topics.pushUnreadCount(targetUid); + + return { cids }; +}; + categoriesAPI.getPrivileges = async (caller, { cid }) => { await hasAdminPrivilege(caller.uid, 'privileges'); diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 8d4c0aebcf..e987b433ce 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -1,6 +1,7 @@ 'use strict'; const categories = require('../../categories'); +const meta = require('../../meta'); const api = require('../../api'); const helpers = require('../helpers'); @@ -44,6 +45,24 @@ Categories.getPosts = async (req, res) => { helpers.formatApiResponse(200, res, posts); }; +Categories.setWatchState = async (req, res) => { + const { cid } = req.params; + let { uid, state } = req.body; + + if (req.method === 'DELETE') { + // DELETE is always setting state to system default in acp + state = categories.watchStates[meta.config.categoryWatchState]; + } else if (Object.keys(categories.watchStates).includes(state)) { + state = categories.watchStates[state]; // convert to integer for backend processing + } else { + throw new Error('[[error:invalid-data]]'); + } + + const { cids: modified } = await api.categories.setWatchState(req, { cid, state, uid }); + + helpers.formatApiResponse(200, res, { modified }); +}; + Categories.getPrivileges = async (req, res) => { const privilegeSet = await api.categories.getPrivileges(req, { cid: req.params.cid }); helpers.formatApiResponse(200, res, privilegeSet); diff --git a/src/middleware/assert.js b/src/middleware/assert.js index 553114f870..6c0f5ef72f 100644 --- a/src/middleware/assert.js +++ b/src/middleware/assert.js @@ -11,6 +11,7 @@ const nconf = require('nconf'); const file = require('../file'); const user = require('../user'); const groups = require('../groups'); +const categories = require('../categories'); const topics = require('../topics'); const posts = require('../posts'); const messaging = require('../messaging'); @@ -39,6 +40,14 @@ Assert.group = helpers.try(async (req, res, next) => { next(); }); +Assert.category = helpers.try(async (req, res, next) => { + if (!await categories.exists(req.params.cid)) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-category]]')); + } + + next(); +}); + Assert.topic = helpers.try(async (req, res, next) => { if (!await topics.exists(req.params.tid)) { return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-topic]]')); diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 44a3020863..f1e2f39504 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -19,6 +19,9 @@ module.exports = function () { setupApiRoute(router, 'get', '/:cid/count', [...middlewares], controllers.write.categories.getTopicCount); setupApiRoute(router, 'get', '/:cid/posts', [...middlewares], controllers.write.categories.getPosts); + setupApiRoute(router, 'put', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); + setupApiRoute(router, 'delete', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); + setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges); setupApiRoute(router, 'put', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); setupApiRoute(router, 'delete', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index aea08cdfa2..e2b647f71a 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -112,12 +112,16 @@ SocketCategories.getSelectCategories = async function (socket) { }; SocketCategories.setWatchState = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT/DELETE /api/v3/categories/:cid/watch'); + if (!data || !data.cid || !data.state) { throw new Error('[[error:invalid-data]]'); } - return await ignoreOrWatch(async (uid, cids) => { - await user.setCategoryWatchState(uid, cids, categories.watchStates[data.state]); - }, socket, data); + + data.state = categories.watchStates[data.state]; + + await api.categories.setWatchState(socket, data); + return data.cid; }; SocketCategories.watch = async function (socket, data) {