diff --git a/install/package.json b/install/package.json index 8ff948229b..8f4aa6c862 100644 --- a/install/package.json +++ b/install/package.json @@ -103,10 +103,10 @@ "nodebb-plugin-ntfy": "1.7.4", "nodebb-plugin-spam-be-gone": "2.2.2", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.52", + "nodebb-theme-harmony": "1.2.53", "nodebb-theme-lavender": "7.1.8", "nodebb-theme-peace": "2.2.4", - "nodebb-theme-persona": "13.3.17", + "nodebb-theme-persona": "13.3.18", "nodebb-widget-essentials": "7.0.15", "nodemailer": "6.9.13", "nprogress": "0.2.0", diff --git a/public/src/client/account/info.js b/public/src/client/account/info.js index f99bd7c36d..f044860cd6 100644 --- a/public/src/client/account/info.js +++ b/public/src/client/account/info.js @@ -12,12 +12,16 @@ define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/s function handleModerationNote() { $('[component="account/save-moderation-note"]').on('click', function () { - const note = $('[component="account/moderation-note"]').val(); - socket.emit('user.setModerationNote', { uid: ajaxify.data.uid, note: note }, function (err, notes) { + const noteEl = $('[component="account/moderation-note"]'); + const note = noteEl.val(); + socket.emit('user.setModerationNote', { + uid: ajaxify.data.uid, + note: note, + }, function (err, notes) { if (err) { return alerts.error(err); } - $('[component="account/moderation-note"]').val(''); + noteEl.val(''); app.parseAndTranslate('account/info', 'moderationNotes', { moderationNotes: notes }, function (html) { $('[component="account/moderation-note/list"]').prepend(html); @@ -25,6 +29,54 @@ define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/s }); }); }); + + + $('[component="account/moderation-note/edit"]').on('click', function () { + const parent = $(this).parents('[data-id]'); + const contentArea = parent.find('[component="account/moderation-note/content-area"]'); + const editArea = parent.find('[component="account/moderation-note/edit-area"]'); + contentArea.addClass('hidden'); + editArea.removeClass('hidden'); + editArea.find('textarea').trigger('focus').putCursorAtEnd(); + }); + + $('[component="account/moderation-note/save-edit"]').on('click', function () { + const parent = $(this).parents('[data-id]'); + const contentArea = parent.find('[component="account/moderation-note/content-area"]'); + const editArea = parent.find('[component="account/moderation-note/edit-area"]'); + contentArea.removeClass('hidden'); + const textarea = editArea.find('textarea'); + + socket.emit('user.editModerationNote', { + uid: ajaxify.data.uid, + id: parent.attr('data-id'), + note: textarea.val(), + }, function (err, notes) { + if (err) { + return alerts.error(err); + } + textarea.css({ + height: textarea.prop('scrollHeight') + 'px', + }); + editArea.addClass('hidden'); + contentArea.find('.content').html(notes[0].note); + }); + }); + + $('[component="account/moderation-note/cancel-edit"]').on('click', function () { + const parent = $(this).parents('[data-id]'); + const contentArea = parent.find('[component="account/moderation-note/content-area"]'); + const editArea = parent.find('[component="account/moderation-note/edit-area"]'); + contentArea.removeClass('hidden'); + editArea.addClass('hidden'); + }); + + $('[component="account/moderation-note/edit-area"] textarea').each((i, el) => { + const $el = $(el); + $el.css({ + height: $el.prop('scrollHeight') + 'px', + }).parent().addClass('hidden'); + }); } return Info; diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 63620247f9..51e5dc9f71 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -155,6 +155,26 @@ SocketUser.setModerationNote = async function (socket, data) { return await user.getModerationNotes(data.uid, 0, 0); }; +SocketUser.editModerationNote = async function (socket, data) { + if (!socket.uid || !data || !data.uid || !data.note || !data.id) { + throw new Error('[[error:invalid-data]]'); + } + const noteData = { + note: data.note, + timestamp: data.id, + }; + let canEdit = await privileges.users.canEdit(socket.uid, data.uid); + if (!canEdit) { + canEdit = await user.isModeratorOfAnyCategory(socket.uid); + } + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + await user.setModerationNote({ uid: data.uid, noteData }); + return await user.getModerationNotesByIds(data.uid, [data.id]); +}; + SocketUser.deleteUpload = async function (socket, data) { if (!data || !data.name || !data.uid) { throw new Error('[[error:invalid-data]]'); diff --git a/src/user/info.js b/src/user/info.js index 0fd4d756aa..47de1154fb 100644 --- a/src/user/info.js +++ b/src/user/info.js @@ -112,12 +112,17 @@ module.exports = function (User) { User.getModerationNotes = async function (uid, start, stop) { const noteIds = await db.getSortedSetRevRange(`uid:${uid}:moderation:notes`, start, stop); + return await User.getModerationNotesByIds(uid, noteIds); + }; + + User.getModerationNotesByIds = async (uid, noteIds) => { const keys = noteIds.map(id => `uid:${uid}:moderation:note:${id}`); const notes = await db.getObjects(keys); const uids = []; - notes.forEach((note) => { + notes.forEach((note, idx) => { if (note) { + note.id = noteIds[idx]; uids.push(note.uid); note.timestampISO = utils.toISOString(note.timestamp); } @@ -125,6 +130,7 @@ module.exports = function (User) { const userData = await User.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture']); await Promise.all(notes.map(async (note, index) => { if (note) { + note.rawNote = validator.escape(String(note.note)); note.note = await plugins.hooks.fire('filter:parse.raw', String(note.note)); note.user = userData[index]; } @@ -136,4 +142,8 @@ module.exports = function (User) { await db.sortedSetAdd(`uid:${uid}:moderation:notes`, noteData.timestamp, noteData.timestamp); await db.setObject(`uid:${uid}:moderation:note:${noteData.timestamp}`, noteData); }; + + User.setModerationNote = async ({ uid, noteData }) => { + await db.setObject(`uid:${uid}:moderation:note:${noteData.timestamp}`, noteData); + }; };