mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: #9506, allow seeing and editing your queued posts
allow regular users access to post queue allow regular users to edit their queued post/topic title allow regular users to remove their post from post queue ability to send a notification to user without removing from post queue allow accessing single post queue items from notifications
This commit is contained in:
@@ -72,6 +72,8 @@
|
|||||||
"bootbox.ok": "OK",
|
"bootbox.ok": "OK",
|
||||||
"bootbox.cancel": "Cancel",
|
"bootbox.cancel": "Cancel",
|
||||||
"bootbox.confirm": "Confirm",
|
"bootbox.confirm": "Confirm",
|
||||||
|
"bootbox.submit": "Submit",
|
||||||
|
"bootbox.send": "Send",
|
||||||
|
|
||||||
"cover.dragging_title": "Cover Photo Positioning",
|
"cover.dragging_title": "Cover Photo Positioning",
|
||||||
"cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"",
|
"cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"users-csv-exported": "Users csv exported, click to download",
|
"users-csv-exported": "Users csv exported, click to download",
|
||||||
"post-queue-accepted": "Your queued post has been accepted. Click here to see your post.",
|
"post-queue-accepted": "Your queued post has been accepted. Click here to see your post.",
|
||||||
"post-queue-rejected": "Your queued post has been rejected.",
|
"post-queue-rejected": "Your queued post has been rejected.",
|
||||||
|
"post-queue-notify": "Queued post received a notification:<br/>\"%1\"",
|
||||||
|
|
||||||
"email-confirmed": "Email Confirmed",
|
"email-confirmed": "Email Confirmed",
|
||||||
"email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
|
"email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
|
||||||
|
|||||||
@@ -14,5 +14,8 @@
|
|||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"topic": "Topic",
|
"topic": "Topic",
|
||||||
"accept": "Accept",
|
"accept": "Accept",
|
||||||
"reject": "Reject"
|
"reject": "Reject",
|
||||||
|
"remove": "Remove",
|
||||||
|
"notify": "Notify",
|
||||||
|
"notify-user": "Notify User"
|
||||||
}
|
}
|
||||||
@@ -238,6 +238,8 @@ paths:
|
|||||||
$ref: 'read/flags/flagId.yaml'
|
$ref: 'read/flags/flagId.yaml'
|
||||||
/api/post-queue:
|
/api/post-queue:
|
||||||
$ref: 'read/post-queue.yaml'
|
$ref: 'read/post-queue.yaml'
|
||||||
|
"/api/post-queue/{id}":
|
||||||
|
$ref: 'read/post-queue.yaml'
|
||||||
/api/ip-blacklist:
|
/api/ip-blacklist:
|
||||||
$ref: 'read/ip-blacklist.yaml'
|
$ref: 'read/ip-blacklist.yaml'
|
||||||
/api/registration-queue:
|
/api/registration-queue:
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
|
|
||||||
define('forum/post-queue', [
|
define('forum/post-queue', [
|
||||||
'categoryFilter', 'categorySelector', 'api', 'alerts',
|
'categoryFilter', 'categorySelector', 'api', 'alerts', 'bootbox',
|
||||||
], function (categoryFilter, categorySelector, api, alerts) {
|
], function (categoryFilter, categorySelector, api, alerts, bootbox) {
|
||||||
const PostQueue = {};
|
const PostQueue = {};
|
||||||
|
|
||||||
PostQueue.init = function () {
|
PostQueue.init = function () {
|
||||||
@@ -13,23 +13,45 @@ define('forum/post-queue', [
|
|||||||
privilege: 'moderate',
|
privilege: 'moderate',
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.posts-list').on('click', '[data-action]', function () {
|
$('.posts-list').on('click', '[data-action]', async function () {
|
||||||
|
function getMessage() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const modal = bootbox.dialog({
|
||||||
|
title: '[[post-queue:notify-user]]',
|
||||||
|
message: '<textarea class="form-control"></textarea>',
|
||||||
|
buttons: {
|
||||||
|
OK: {
|
||||||
|
label: '[[modules:bootbox.send]]',
|
||||||
|
callback: function () {
|
||||||
|
const val = modal.find('textarea').val();
|
||||||
|
if (val) {
|
||||||
|
resolve(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
const parent = $(this).parents('[data-id]');
|
const parent = $(this).parents('[data-id]');
|
||||||
const action = $(this).attr('data-action');
|
const action = $(this).attr('data-action');
|
||||||
const id = parent.attr('data-id');
|
const id = parent.attr('data-id');
|
||||||
const listContainer = parent.get(0).parentNode;
|
const listContainer = parent.get(0).parentNode;
|
||||||
|
|
||||||
if (!['accept', 'reject'].some(function (valid) {
|
if (!['accept', 'reject', 'notify'].includes(action)) {
|
||||||
return action === valid;
|
|
||||||
})) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit('posts.' + action, { id: id }, function (err) {
|
socket.emit('posts.' + action, {
|
||||||
|
id: id,
|
||||||
|
message: action === 'notify' ? await getMessage() : undefined,
|
||||||
|
}, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return alerts.error(err);
|
return alerts.error(err);
|
||||||
}
|
}
|
||||||
parent.remove();
|
if (action === 'accept' || action === 'reject') {
|
||||||
|
parent.remove();
|
||||||
|
}
|
||||||
|
|
||||||
if (listContainer.childElementCount === 0) {
|
if (listContainer.childElementCount === 0) {
|
||||||
ajaxify.refresh();
|
ajaxify.refresh();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const validator = require('validator');
|
||||||
|
|
||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
const flags = require('../flags');
|
const flags = require('../flags');
|
||||||
@@ -149,29 +151,25 @@ modsController.flags.detail = async function (req, res, next) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
modsController.postQueue = async function (req, res, next) {
|
modsController.postQueue = async function (req, res, next) {
|
||||||
// Admins, global mods, and individual mods only
|
if (!req.loggedIn) {
|
||||||
const isPrivileged = await user.isPrivileged(req.uid);
|
|
||||||
if (!isPrivileged) {
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
const { id } = req.params;
|
||||||
const { cid } = req.query;
|
const { cid } = req.query;
|
||||||
const page = parseInt(req.query.page, 10) || 1;
|
const page = parseInt(req.query.page, 10) || 1;
|
||||||
const postsPerPage = 20;
|
const postsPerPage = 20;
|
||||||
|
|
||||||
let postData = await posts.getQueuedPosts();
|
let postData = await posts.getQueuedPosts({ id: id });
|
||||||
const [isAdminOrGlobalMod, moderatedCids, categoriesData] = await Promise.all([
|
const [isAdmin, isGlobalMod, moderatedCids, categoriesData] = await Promise.all([
|
||||||
user.isAdminOrGlobalMod(req.uid),
|
user.isAdministrator(req.uid),
|
||||||
|
user.isGlobalModerator(req.uid),
|
||||||
user.getModeratedCids(req.uid),
|
user.getModeratedCids(req.uid),
|
||||||
helpers.getSelectedCategory(cid),
|
helpers.getSelectedCategory(cid),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (cid && !moderatedCids.includes(Number(cid)) && !isAdminOrGlobalMod) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
postData = postData.filter(p => p &&
|
postData = postData.filter(p => p &&
|
||||||
(!categoriesData.selectedCids.length || categoriesData.selectedCids.includes(p.category.cid)) &&
|
(!categoriesData.selectedCids.length || categoriesData.selectedCids.includes(p.category.cid)) &&
|
||||||
(isAdminOrGlobalMod || moderatedCids.includes(Number(p.category.cid))));
|
(isAdmin || isGlobalMod || moderatedCids.includes(Number(p.category.cid)) || req.uid === p.user.uid));
|
||||||
|
|
||||||
({ posts: postData } = await plugins.hooks.fire('filter:post-queue.get', {
|
({ posts: postData } = await plugins.hooks.fire('filter:post-queue.get', {
|
||||||
posts: postData,
|
posts: postData,
|
||||||
@@ -182,13 +180,19 @@ modsController.postQueue = async function (req, res, next) {
|
|||||||
const start = (page - 1) * postsPerPage;
|
const start = (page - 1) * postsPerPage;
|
||||||
const stop = start + postsPerPage - 1;
|
const stop = start + postsPerPage - 1;
|
||||||
postData = postData.slice(start, stop + 1);
|
postData = postData.slice(start, stop + 1);
|
||||||
|
const crumbs = [{ text: '[[pages:post-queue]]', url: id ? '/post-queue' : undefined }];
|
||||||
|
if (id && postData.length) {
|
||||||
|
const text = postData[0].data.tid ? '[[post-queue:reply]]' : '[[post-queue:topic]]';
|
||||||
|
crumbs.push({ text: text });
|
||||||
|
}
|
||||||
res.render('post-queue', {
|
res.render('post-queue', {
|
||||||
title: '[[pages:post-queue]]',
|
title: '[[pages:post-queue]]',
|
||||||
posts: postData,
|
posts: postData,
|
||||||
|
isAdmin: isAdmin,
|
||||||
|
canAccept: isAdmin || isGlobalMod || !!moderatedCids.length,
|
||||||
...categoriesData,
|
...categoriesData,
|
||||||
allCategoriesUrl: `post-queue${helpers.buildQueryString(req.query, 'cid', '')}`,
|
allCategoriesUrl: `post-queue${helpers.buildQueryString(req.query, 'cid', '')}`,
|
||||||
pagination: pagination.create(page, pageCount),
|
pagination: pagination.create(page, pageCount),
|
||||||
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:post-queue]]' }]),
|
breadcrumbs: helpers.buildBreadcrumbs(crumbs),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ middleware.renderHeader = async function renderHeader(req, res, data) {
|
|||||||
'brand:logo:display': meta.config['brand:logo'] ? '' : 'hide',
|
'brand:logo:display': meta.config['brand:logo'] ? '' : 'hide',
|
||||||
allowRegistration: registrationType === 'normal',
|
allowRegistration: registrationType === 'normal',
|
||||||
searchEnabled: plugins.hooks.hasListeners('filter:search.query'),
|
searchEnabled: plugins.hooks.hasListeners('filter:search.query'),
|
||||||
|
postQueueEnabled: !!meta.config.postQueue,
|
||||||
config: res.locals.config,
|
config: res.locals.config,
|
||||||
relative_path,
|
relative_path,
|
||||||
bodyClass: data.bodyClass,
|
bodyClass: data.bodyClass,
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ module.exports = function (Posts) {
|
|||||||
});
|
});
|
||||||
cache.set('post-queue', _.cloneDeep(postData));
|
cache.set('post-queue', _.cloneDeep(postData));
|
||||||
}
|
}
|
||||||
|
if (filter.id) {
|
||||||
|
postData = postData.filter(p => p.id === filter.id);
|
||||||
|
}
|
||||||
if (options.metadata) {
|
if (options.metadata) {
|
||||||
await Promise.all(postData.map(p => addMetaData(p)));
|
await Promise.all(postData.map(p => addMetaData(p)));
|
||||||
}
|
}
|
||||||
@@ -161,7 +163,7 @@ module.exports = function (Posts) {
|
|||||||
mergeId: 'post-queue',
|
mergeId: 'post-queue',
|
||||||
bodyShort: '[[notifications:post_awaiting_review]]',
|
bodyShort: '[[notifications:post_awaiting_review]]',
|
||||||
bodyLong: bodyLong,
|
bodyLong: bodyLong,
|
||||||
path: '/post-queue',
|
path: `/post-queue/${id}`,
|
||||||
});
|
});
|
||||||
await notifications.push(notifObj, uids);
|
await notifications.push(notifObj, uids);
|
||||||
return {
|
return {
|
||||||
@@ -235,7 +237,7 @@ module.exports = function (Posts) {
|
|||||||
Posts.removeFromQueue = async function (id) {
|
Posts.removeFromQueue = async function (id) {
|
||||||
const data = await getParsedObject(id);
|
const data = await getParsedObject(id);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
await removeQueueNotification(id);
|
await removeQueueNotification(id);
|
||||||
await db.sortedSetRemove('post:queue', id);
|
await db.sortedSetRemove('post:queue', id);
|
||||||
@@ -247,7 +249,7 @@ module.exports = function (Posts) {
|
|||||||
Posts.submitFromQueue = async function (id) {
|
Posts.submitFromQueue = async function (id) {
|
||||||
const data = await getParsedObject(id);
|
const data = await getParsedObject(id);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
if (data.type === 'topic') {
|
if (data.type === 'topic') {
|
||||||
const result = await createTopic(data.data);
|
const result = await createTopic(data.data);
|
||||||
@@ -260,6 +262,10 @@ module.exports = function (Posts) {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Posts.getFromQueue = async function (id) {
|
||||||
|
return await getParsedObject(id);
|
||||||
|
};
|
||||||
|
|
||||||
async function getParsedObject(id) {
|
async function getParsedObject(id) {
|
||||||
const data = await db.getObject(`post:queue:${id}`);
|
const data = await db.getObject(`post:queue:${id}`);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -288,7 +294,7 @@ module.exports = function (Posts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Posts.editQueuedContent = async function (uid, editData) {
|
Posts.editQueuedContent = async function (uid, editData) {
|
||||||
const canEditQueue = await Posts.canEditQueue(uid, editData);
|
const canEditQueue = await Posts.canEditQueue(uid, editData, 'edit');
|
||||||
if (!canEditQueue) {
|
if (!canEditQueue) {
|
||||||
throw new Error('[[error:no-privileges]]');
|
throw new Error('[[error:no-privileges]]');
|
||||||
}
|
}
|
||||||
@@ -309,7 +315,7 @@ module.exports = function (Posts) {
|
|||||||
cache.del('post-queue');
|
cache.del('post-queue');
|
||||||
};
|
};
|
||||||
|
|
||||||
Posts.canEditQueue = async function (uid, editData) {
|
Posts.canEditQueue = async function (uid, editData, action) {
|
||||||
const [isAdminOrGlobalMod, data] = await Promise.all([
|
const [isAdminOrGlobalMod, data] = await Promise.all([
|
||||||
user.isAdminOrGlobalMod(uid),
|
user.isAdminOrGlobalMod(uid),
|
||||||
getParsedObject(editData.id),
|
getParsedObject(editData.id),
|
||||||
@@ -317,8 +323,8 @@ module.exports = function (Posts) {
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const selfPost = parseInt(uid, 10) === parseInt(data.uid, 10);
|
||||||
if (isAdminOrGlobalMod) {
|
if (isAdminOrGlobalMod || ((action === 'reject' || action === 'edit') && selfPost)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ _mounts.main = (app, middleware, controllers) => {
|
|||||||
_mounts.mod = (app, middleware, controllers) => {
|
_mounts.mod = (app, middleware, controllers) => {
|
||||||
setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list);
|
setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list);
|
||||||
setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail);
|
setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail);
|
||||||
setupPageRoute(app, '/post-queue', middleware, [], controllers.mods.postQueue);
|
setupPageRoute(app, '/post-queue/:id?', middleware, [], controllers.mods.postQueue);
|
||||||
};
|
};
|
||||||
|
|
||||||
_mounts.globalMod = (app, middleware, controllers) => {
|
_mounts.globalMod = (app, middleware, controllers) => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const validator = require('validator');
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
@@ -100,29 +102,41 @@ SocketPosts.getReplies = async function (socket, pid) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SocketPosts.accept = async function (socket, data) {
|
SocketPosts.accept = async function (socket, data) {
|
||||||
const result = await acceptOrReject(posts.submitFromQueue, socket, data);
|
await canEditQueue(socket, data, 'accept');
|
||||||
await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`);
|
const result = await posts.submitFromQueue(data.id);
|
||||||
|
if (result && socket.uid !== parseInt(result.uid, 10)) {
|
||||||
|
await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
SocketPosts.reject = async function (socket, data) {
|
SocketPosts.reject = async function (socket, data) {
|
||||||
const result = await acceptOrReject(posts.removeFromQueue, socket, data);
|
await canEditQueue(socket, data, 'reject');
|
||||||
await sendQueueNotification('post-queue-rejected', result.uid, '/');
|
const result = await posts.removeFromQueue(data.id);
|
||||||
|
if (result && socket.uid !== parseInt(result.uid, 10)) {
|
||||||
|
await sendQueueNotification('post-queue-rejected', result.uid, '/');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function acceptOrReject(method, socket, data) {
|
SocketPosts.notify = async function (socket, data) {
|
||||||
const canEditQueue = await posts.canEditQueue(socket.uid, data);
|
await canEditQueue(socket, data, 'notify');
|
||||||
|
const result = await posts.getFromQueue(data.id);
|
||||||
|
if (result) {
|
||||||
|
await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function canEditQueue(socket, data, action) {
|
||||||
|
const canEditQueue = await posts.canEditQueue(socket.uid, data, action);
|
||||||
if (!canEditQueue) {
|
if (!canEditQueue) {
|
||||||
throw new Error('[[error:no-privileges]]');
|
throw new Error('[[error:no-privileges]]');
|
||||||
}
|
}
|
||||||
return await method(data.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendQueueNotification(type, targetUid, path) {
|
async function sendQueueNotification(type, targetUid, path, notificationText) {
|
||||||
const notifData = {
|
const notifData = {
|
||||||
type: type,
|
type: type,
|
||||||
nid: `${type}-${targetUid}-${path}`,
|
nid: `${type}-${targetUid}-${path}`,
|
||||||
bodyShort: type === 'post-queue-accepted' ?
|
bodyShort: `[[notifications:post-queue-notify, ${notificationText}]]` || `[[notifications:${type}]]`,
|
||||||
'[[notifications:post-queue-accepted]]' : '[[notifications:post-queue-rejected]]',
|
|
||||||
path: path,
|
path: path,
|
||||||
};
|
};
|
||||||
if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) {
|
if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user