mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-01 21:30:30 +01:00
feat: closes #9684, allow event deletion
fix: topic events appearing before necro messages feat: add move topic event feat: add ability to delete specific topic events via events.purge
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"login-to-view": "🔒 Log in to view",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"delete-event": "Delete Event",
|
||||
"purge": "Purge",
|
||||
"restore": "Restore",
|
||||
"move": "Move",
|
||||
@@ -47,6 +48,7 @@
|
||||
"unpinned-by": "Unpinned by",
|
||||
"deleted-by": "Deleted by",
|
||||
"restored-by": "Restored by",
|
||||
"moved-from-by": "Moved from %1 by",
|
||||
"queued-by": "Post queued for approval →",
|
||||
|
||||
"bookmark_instructions" : "Click here to return to the last read post in this thread.",
|
||||
|
||||
@@ -112,6 +112,8 @@ paths:
|
||||
$ref: 'write/topics/tid/thumbs/order.yaml'
|
||||
/topics/{tid}/events:
|
||||
$ref: 'write/topics/tid/events.yaml'
|
||||
/topics/{tid}/events/{eventId}:
|
||||
$ref: 'write/topics/tid/events/eventId.yaml'
|
||||
/posts/{pid}:
|
||||
$ref: 'write/posts/pid.yaml'
|
||||
/posts/{pid}/state:
|
||||
|
||||
33
public/openapi/write/topics/tid/events/eventId.yaml
Normal file
33
public/openapi/write/topics/tid/events/eventId.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
delete:
|
||||
tags:
|
||||
- topics
|
||||
summary: Delete a topic event
|
||||
description: This operation deletes a single topic event from the topic
|
||||
parameters:
|
||||
- in: path
|
||||
name: tid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid topic id
|
||||
example: 1
|
||||
- in: path
|
||||
name: eventId
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid topic event id
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Topic event successfully deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties: {}
|
||||
@@ -279,8 +279,7 @@ define('forum/topic/posts', [
|
||||
posts.find('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive');
|
||||
Posts.addBlockquoteEllipses(posts);
|
||||
hidePostToolsForDeletedPosts(posts);
|
||||
Posts.addTopicEvents();
|
||||
addNecroPostMessage();
|
||||
addNecroPostMessage(Posts.addTopicEvents);
|
||||
};
|
||||
|
||||
Posts.addTopicEvents = function (events) {
|
||||
@@ -316,6 +315,7 @@ define('forum/topic/posts', [
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
event.isAdminOrMod = ajaxify.data.privileges.isAdminOrMod;
|
||||
app.parseAndTranslate('partials/topic/event', event, function (html) {
|
||||
html = html.get(0);
|
||||
|
||||
@@ -333,14 +333,15 @@ define('forum/topic/posts', [
|
||||
});
|
||||
};
|
||||
|
||||
function addNecroPostMessage() {
|
||||
function addNecroPostMessage(callback) {
|
||||
var necroThreshold = ajaxify.data.necroThreshold * 24 * 60 * 60 * 1000;
|
||||
if (!necroThreshold || (config.topicPostSort !== 'newest_to_oldest' && config.topicPostSort !== 'oldest_to_newest')) {
|
||||
return;
|
||||
return callback && callback();
|
||||
}
|
||||
|
||||
$('[component="post"]').each(function () {
|
||||
var post = $(this);
|
||||
var postEls = $('[component="post"]').toArray();
|
||||
Promise.all(postEls.map(function (post) {
|
||||
post = $(post);
|
||||
var prev = post.prev('[component="post"]');
|
||||
if (post.is(':has(.necro-post)') || !prev.length) {
|
||||
return;
|
||||
@@ -350,27 +351,34 @@ define('forum/topic/posts', [
|
||||
}
|
||||
|
||||
var diff = post.attr('data-timestamp') - prev.attr('data-timestamp');
|
||||
if (Math.abs(diff) >= necroThreshold) {
|
||||
var suffixAgo = $.timeago.settings.strings.suffixAgo;
|
||||
var prefixAgo = $.timeago.settings.strings.prefixAgo;
|
||||
var suffixFromNow = $.timeago.settings.strings.suffixFromNow;
|
||||
var prefixFromNow = $.timeago.settings.strings.prefixFromNow;
|
||||
return new Promise(function (resolve) {
|
||||
if (Math.abs(diff) >= necroThreshold) {
|
||||
var suffixAgo = $.timeago.settings.strings.suffixAgo;
|
||||
var prefixAgo = $.timeago.settings.strings.prefixAgo;
|
||||
var suffixFromNow = $.timeago.settings.strings.suffixFromNow;
|
||||
var prefixFromNow = $.timeago.settings.strings.prefixFromNow;
|
||||
|
||||
$.timeago.settings.strings.suffixAgo = '';
|
||||
$.timeago.settings.strings.prefixAgo = '';
|
||||
$.timeago.settings.strings.suffixFromNow = '';
|
||||
$.timeago.settings.strings.prefixFromNow = '';
|
||||
$.timeago.settings.strings.suffixAgo = '';
|
||||
$.timeago.settings.strings.prefixAgo = '';
|
||||
$.timeago.settings.strings.suffixFromNow = '';
|
||||
$.timeago.settings.strings.prefixFromNow = '';
|
||||
|
||||
var translationText = (diff > 0 ? '[[topic:timeago_later,' : '[[topic:timeago_earlier,') + $.timeago.inWords(diff) + ']]';
|
||||
var translationText = (diff > 0 ? '[[topic:timeago_later,' : '[[topic:timeago_earlier,') + $.timeago.inWords(diff) + ']]';
|
||||
|
||||
$.timeago.settings.strings.suffixAgo = suffixAgo;
|
||||
$.timeago.settings.strings.prefixAgo = prefixAgo;
|
||||
$.timeago.settings.strings.suffixFromNow = suffixFromNow;
|
||||
$.timeago.settings.strings.prefixFromNow = prefixFromNow;
|
||||
app.parseAndTranslate('partials/topic/necro-post', { text: translationText }, function (html) {
|
||||
html.insertBefore(post);
|
||||
});
|
||||
}
|
||||
$.timeago.settings.strings.suffixAgo = suffixAgo;
|
||||
$.timeago.settings.strings.prefixAgo = prefixAgo;
|
||||
$.timeago.settings.strings.suffixFromNow = suffixFromNow;
|
||||
$.timeago.settings.strings.prefixFromNow = prefixFromNow;
|
||||
app.parseAndTranslate('partials/topic/necro-post', { text: translationText }, function (html) {
|
||||
html.insertBefore(post);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})).then(function () {
|
||||
callback && callback();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,17 @@ define('forum/topic/threadTools', [
|
||||
return false;
|
||||
});
|
||||
|
||||
topicContainer.on('click', '[component="topic/event/delete"]', function () {
|
||||
const eventId = $(this).attr('data-topic-event-id');
|
||||
const eventEl = $(this).parents('[component="topic/event"]');
|
||||
api.del(`/topics/${tid}/events/${eventId}`, {})
|
||||
.then(function () {
|
||||
eventEl.remove();
|
||||
})
|
||||
.catch(app.alertError);
|
||||
return false;
|
||||
});
|
||||
|
||||
// todo: should also use topicCommand, but no write api call exists for this yet
|
||||
topicContainer.on('click', '[component="topic/mark-unread"]', function () {
|
||||
socket.emit('topics.markUnread', tid, function (err) {
|
||||
|
||||
@@ -211,3 +211,11 @@ Topics.getEvents = async (req, res) => {
|
||||
|
||||
helpers.formatApiResponse(200, res, await topics.events.get(req.params.tid));
|
||||
};
|
||||
|
||||
Topics.deleteEvent = async (req, res) => {
|
||||
if (!await privileges.topics.isAdminOrMod(req.params.tid, req.uid)) {
|
||||
return helpers.formatApiResponse(403, res);
|
||||
}
|
||||
await topics.events.purge(req.params.tid, [req.params.eventId]);
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ module.exports = function () {
|
||||
setupApiRoute(router, 'put', '/:tid/thumbs/order', [...middlewares, middleware.checkRequired.bind(null, ['path', 'order'])], controllers.write.topics.reorderThumbs);
|
||||
|
||||
setupApiRoute(router, 'get', '/:tid/events', [middleware.assert.topic], controllers.write.topics.getEvents);
|
||||
setupApiRoute(router, 'delete', '/:tid/events/:eventId', [middleware.assert.topic], controllers.write.topics.deleteEvent);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const db = require('../database');
|
||||
const user = require('../user');
|
||||
const posts = require('../posts');
|
||||
const categories = require('../categories');
|
||||
const plugins = require('../plugins');
|
||||
|
||||
const Events = module.exports;
|
||||
@@ -42,6 +44,10 @@ Events._types = {
|
||||
icon: 'fa-trash-o',
|
||||
text: '[[topic:restored-by]]',
|
||||
},
|
||||
move: {
|
||||
icon: 'fa-arrow-circle-right',
|
||||
// text: '[[topic:moved-from-by]]',
|
||||
},
|
||||
'post-queue': {
|
||||
icon: 'fa-history',
|
||||
text: '[[topic:queued-by]]',
|
||||
@@ -83,6 +89,12 @@ async function getUserInfo(uids) {
|
||||
return userMap;
|
||||
}
|
||||
|
||||
async function getCategoryInfo(cids) {
|
||||
const uniqCids = _.uniq(cids);
|
||||
const catData = await categories.getCategoriesFields(uniqCids, ['name', 'slug', 'icon', 'color', 'bgColor']);
|
||||
return _.zipObject(uniqCids, catData);
|
||||
}
|
||||
|
||||
async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
|
||||
// Add posts from post queue
|
||||
const isPrivileged = await user.isPrivileged(uid);
|
||||
@@ -98,7 +110,10 @@ async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
|
||||
});
|
||||
}
|
||||
|
||||
const users = await getUserInfo(events.map(event => event.uid).filter(Boolean));
|
||||
const [users, fromCategories] = await Promise.all([
|
||||
getUserInfo(events.map(event => event.uid).filter(Boolean)),
|
||||
getCategoryInfo(events.map(event => event.fromCid).filter(Boolean)),
|
||||
]);
|
||||
|
||||
// Remove events whose types no longer exist (e.g. plugin uninstalled)
|
||||
events = events.filter(event => Events._types.hasOwnProperty(event.type));
|
||||
@@ -111,6 +126,10 @@ async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
|
||||
if (event.hasOwnProperty('uid')) {
|
||||
event.user = users.get(event.uid === 'system' ? 'system' : parseInt(event.uid, 10));
|
||||
}
|
||||
if (event.hasOwnProperty('fromCid')) {
|
||||
event.fromCategory = fromCategories[event.fromCid];
|
||||
event.text = `[[topic:moved-from-by, ${event.fromCategory.name}]]`;
|
||||
}
|
||||
|
||||
Object.assign(event, Events._types[event.type]);
|
||||
});
|
||||
@@ -149,11 +168,19 @@ Events.log = async (tid, payload) => {
|
||||
return events;
|
||||
};
|
||||
|
||||
Events.purge = async (tid) => {
|
||||
// Should only be called on topic purge
|
||||
const keys = [`topic:${tid}:events`];
|
||||
const eventIds = await db.getSortedSetRange(keys[0], 0, -1);
|
||||
keys.push(...eventIds.map(id => `topicEvent:${id}`));
|
||||
Events.purge = async (tid, eventIds = []) => {
|
||||
if (eventIds.length) {
|
||||
const isTopicEvent = await db.isSortedSetMembers(`topic:${tid}:events`, eventIds);
|
||||
eventIds = eventIds.filter((id, index) => isTopicEvent[index]);
|
||||
await Promise.all([
|
||||
db.sortedSetRemove(`topic:${tid}:events`, eventIds),
|
||||
db.deleteAll(eventIds.map(id => `topicEvent:${id}`)),
|
||||
]);
|
||||
} else {
|
||||
const keys = [`topic:${tid}:events`];
|
||||
const eventIds = await db.getSortedSetRange(keys[0], 0, -1);
|
||||
keys.push(...eventIds.map(id => `topicEvent:${id}`));
|
||||
|
||||
await db.deleteAll(keys);
|
||||
await db.deleteAll(keys);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -268,6 +268,7 @@ module.exports = function (Topics) {
|
||||
oldCid: oldCid,
|
||||
}),
|
||||
Topics.updateCategoryTagsCount([oldCid, cid], tags),
|
||||
Topics.events.log(tid, { type: 'move', uid: data.uid, fromCid: oldCid }),
|
||||
]);
|
||||
const hookData = _.clone(data);
|
||||
hookData.fromCid = oldCid;
|
||||
|
||||
Reference in New Issue
Block a user