diff --git a/install/package.json b/install/package.json index dea345ec0b..0186eff62e 100644 --- a/install/package.json +++ b/install/package.json @@ -107,10 +107,10 @@ "nodebb-plugin-ntfy": "1.7.7", "nodebb-plugin-spam-be-gone": "2.2.2", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.77", + "nodebb-theme-harmony": "1.2.78", "nodebb-theme-lavender": "7.1.10", "nodebb-theme-peace": "2.2.8", - "nodebb-theme-persona": "13.3.40", + "nodebb-theme-persona": "13.3.41", "nodebb-widget-essentials": "7.0.30", "nodemailer": "6.9.16", "nprogress": "0.2.0", diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index 9027774603..fdb6ff6e91 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -30,6 +30,7 @@ "restore": "Restore", "move": "Move", "change-owner": "Change Owner", + "manage-editors": "Manage Editors", "fork": "Fork", "link": "Link", "share": "Share", @@ -116,6 +117,7 @@ "thread-tools.move-posts": "Move Posts", "thread-tools.move-all": "Move All", "thread-tools.change-owner": "Change Owner", + "thread-tools.manage-editors": "Manage Editors", "thread-tools.select-category": "Select Category", "thread-tools.fork": "Fork Topic", "thread-tools.tag": "Tag Topic", @@ -177,6 +179,7 @@ "move-posts-instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", "move-topic-instruction": "Select the target category and then click move", "change-owner-instruction": "Click the posts you want to assign to another user", + "manage-editors-instruction": "Manage the users who can edit this post below.", "composer.title-placeholder": "Enter your topic title here...", "composer.handle-placeholder": "Enter your name/handle here", diff --git a/public/src/client/topic/manage-editors.js b/public/src/client/topic/manage-editors.js new file mode 100644 index 0000000000..4d063bfa26 --- /dev/null +++ b/public/src/client/topic/manage-editors.js @@ -0,0 +1,77 @@ +'use strict'; + + +define('forum/topic/manage-editors', [ + 'autocomplete', + 'alerts', +], function (autocomplete, alerts) { + const ManageEditors = {}; + + let modal; + + ManageEditors.init = async function (postEl) { + if (modal) { + return; + } + const pid = postEl.attr('data-pid'); + + let editors = await socket.emit('posts.getEditors', { pid: pid }); + app.parseAndTranslate('modals/manage-editors', { + editors: editors, + }, function (html) { + modal = html; + + const commitEl = modal.find('#manage_editors_commit'); + + $('body').append(modal); + + modal.find('#manage_editors_cancel').on('click', closeModal); + + commitEl.on('click', function () { + saveEditors(pid); + }); + + autocomplete.user(modal.find('#username'), { filters: ['notbanned'] }, function (ev, ui) { + const isInEditors = editors.find(e => String(e.uid) === String(ui.item.user.uid)); + if (!isInEditors) { + editors.push(ui.item.user); + app.parseAndTranslate('modals/manage-editors', 'editors', { + editors: editors, + }, function (html) { + modal.find('[component="topic/editors"]').html(html); + modal.find('#username').val(''); + }); + } + }); + + modal.on('click', 'button.remove-user-icon', function () { + const el = $(this).parents('[data-uid]'); + const uid = el.attr('data-uid'); + editors = editors.filter(e => String(e.uid) === String(uid)); + el.remove(); + }); + }); + }; + + function saveEditors(pid) { + const uids = modal.find('[component="topic/editors"]>[data-uid]') + .map((i, el) => $(el).attr('data-uid')).get(); + + socket.emit('posts.saveEditors', { pid: pid, uids: uids }, function (err) { + if (err) { + return alerts.error(err); + } + + closeModal(); + }); + } + + function closeModal() { + if (modal) { + modal.remove(); + modal = null; + } + } + + return ManageEditors; +}); diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index f8d2ca8933..5a5872c927 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -253,6 +253,13 @@ define('forum/topic/postTools', [ }); }); + postContainer.on('click', '[component="post/manage-editors"]', function () { + const btn = $(this); + require(['forum/topic/manage-editors'], function (manageEditors) { + manageEditors.init(btn.parents('[data-pid]')); + }); + }); + postContainer.on('click', '[component="post/ban-ip"]', function () { const ip = $(this).attr('data-ip'); socket.emit('blacklist.addRule', ip, function (err) { diff --git a/src/posts/delete.js b/src/posts/delete.js index 94f73cf494..7c1efdec52 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -81,6 +81,7 @@ module.exports = function (Posts) { deleteDiffs(pids), deleteFromUploads(pids), db.sortedSetsRemove(['posts:pid', 'posts:votes', 'posts:flagged'], pids), + db.deleteAll(pids.map(pid => `pid:${pid}:editors`)), ]); await resolveFlags(postData, uid); diff --git a/src/privileges/posts.js b/src/privileges/posts.js index fbd6858282..505afc5321 100644 --- a/src/privileges/posts.js +++ b/src/privileges/posts.js @@ -3,6 +3,7 @@ const _ = require('lodash'); +const db = require('../database'); const meta = require('../meta'); const posts = require('../posts'); const topics = require('../topics'); @@ -118,7 +119,8 @@ privsPosts.canEdit = async function (pid, uid) { const results = await utils.promiseParallel({ isAdmin: user.isAdministrator(uid), isMod: posts.isModerator([pid], uid), - owner: posts.isOwner(pid, uid), + isOwner: posts.isOwner(pid, uid), + isEditor: db.isSetMember(`pid:${pid}:editors`, uid), edit: privsPosts.can('posts:edit', pid, uid), postData: posts.getPostFields(pid, ['tid', 'timestamp', 'deleted', 'deleterUid']), userData: user.getUserFields(uid, ['reputation']), @@ -158,7 +160,10 @@ privsPosts.canEdit = async function (pid, uid) { results.uid = uid; const result = await plugins.hooks.fire('filter:privileges.posts.edit', results); - return { flag: result.edit && (result.owner || result.isMod), message: '[[error:no-privileges]]' }; + return { + flag: result.edit && (result.isOwner || result.isEditor || result.isMod), + message: '[[error:no-privileges]]', + }; }; privsPosts.canDelete = async function (pid, uid) { diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js index 44b488216e..ba616f313f 100644 --- a/src/socket.io/posts/tools.js +++ b/src/socket.io/posts/tools.js @@ -46,6 +46,7 @@ module.exports = function (SocketPosts) { postData.display_moderator_tools = postData.display_edit_tools || postData.display_delete_tools; postData.display_move_tools = results.isAdmin || results.isModerator; postData.display_change_owner_tools = results.isAdmin || results.isModerator; + postData.display_manage_editors_tools = results.isAdmin || results.isModerator || postData.selfPost; postData.display_ip_ban = (results.isAdmin || results.isGlobalMod) && !postData.selfPost; postData.display_history = results.history && results.canViewHistory; postData.flags = { @@ -92,4 +93,35 @@ module.exports = function (SocketPosts) { await Promise.all(logs); }; + + SocketPosts.getEditors = async function (socket, data) { + if (!data || !data.pid) { + throw new Error('[[error:invalid-data]]'); + } + await checkEditorPrivilege(socket.uid, data.pid); + const editorUids = await db.getSetMembers(`pid:${data.pid}:editors`); + const userData = await user.getUsersFields(editorUids, ['username', 'userslug', 'picture']); + return userData; + }; + + SocketPosts.saveEditors = async function (socket, data) { + if (!data || !data.pid || !Array.isArray(data.uids)) { + throw new Error('[[error:invalid-data]]'); + } + await checkEditorPrivilege(socket.uid, data.pid); + await db.delete(`pid:${data.pid}:editors`); + await db.setAdd(`pid:${data.pid}:editors`, data.uids); + }; + + async function checkEditorPrivilege(uid, pid) { + const cid = await posts.getCidByPid(pid); + const [isAdminOrMod, owner] = await Promise.all([ + privileges.categories.isAdminOrMod(cid, uid), + posts.getPostField(pid, 'uid'), + ]); + const isSelfPost = String(uid) === String(owner); + if (!isAdminOrMod && !isSelfPost) { + throw new Error('[[error:no-privileges]]'); + } + } }; diff --git a/src/views/modals/manage-editors.tpl b/src/views/modals/manage-editors.tpl new file mode 100644 index 0000000000..9faea98675 --- /dev/null +++ b/src/views/modals/manage-editors.tpl @@ -0,0 +1,32 @@ +
+
[[topic:thread-tools.manage-editors]]
+
+

+ [[topic:manage-editors-instruction]] +

+
+ +
+ + + + +
+
+
+ {{{ each editors }}} +
+ {buildAvatar(@value, "24px", true)} + {./username} + +
+ {{{ end }}} +
+
+ +