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
v1.18.x
Barış Soner Uşaklı 4 years ago
parent 23dafa204c
commit 358ad74054

@ -23,6 +23,7 @@
"login-to-view": "🔒 Log in to view", "login-to-view": "🔒 Log in to view",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"delete-event": "Delete Event",
"purge": "Purge", "purge": "Purge",
"restore": "Restore", "restore": "Restore",
"move": "Move", "move": "Move",
@ -47,6 +48,7 @@
"unpinned-by": "Unpinned by", "unpinned-by": "Unpinned by",
"deleted-by": "Deleted by", "deleted-by": "Deleted by",
"restored-by": "Restored by", "restored-by": "Restored by",
"moved-from-by": "Moved from %1 by",
"queued-by": "Post queued for approval →", "queued-by": "Post queued for approval →",
"bookmark_instructions" : "Click here to return to the last read post in this thread.", "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' $ref: 'write/topics/tid/thumbs/order.yaml'
/topics/{tid}/events: /topics/{tid}/events:
$ref: 'write/topics/tid/events.yaml' $ref: 'write/topics/tid/events.yaml'
/topics/{tid}/events/{eventId}:
$ref: 'write/topics/tid/events/eventId.yaml'
/posts/{pid}: /posts/{pid}:
$ref: 'write/posts/pid.yaml' $ref: 'write/posts/pid.yaml'
/posts/{pid}/state: /posts/{pid}/state:

@ -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.find('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive');
Posts.addBlockquoteEllipses(posts); Posts.addBlockquoteEllipses(posts);
hidePostToolsForDeletedPosts(posts); hidePostToolsForDeletedPosts(posts);
Posts.addTopicEvents(); addNecroPostMessage(Posts.addTopicEvents);
addNecroPostMessage();
}; };
Posts.addTopicEvents = function (events) { Posts.addTopicEvents = function (events) {
@ -316,6 +315,7 @@ define('forum/topic/posts', [
} }
return new Promise((resolve) => { return new Promise((resolve) => {
event.isAdminOrMod = ajaxify.data.privileges.isAdminOrMod;
app.parseAndTranslate('partials/topic/event', event, function (html) { app.parseAndTranslate('partials/topic/event', event, function (html) {
html = html.get(0); 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; var necroThreshold = ajaxify.data.necroThreshold * 24 * 60 * 60 * 1000;
if (!necroThreshold || (config.topicPostSort !== 'newest_to_oldest' && config.topicPostSort !== 'oldest_to_newest')) { if (!necroThreshold || (config.topicPostSort !== 'newest_to_oldest' && config.topicPostSort !== 'oldest_to_newest')) {
return; return callback && callback();
} }
$('[component="post"]').each(function () { var postEls = $('[component="post"]').toArray();
var post = $(this); Promise.all(postEls.map(function (post) {
post = $(post);
var prev = post.prev('[component="post"]'); var prev = post.prev('[component="post"]');
if (post.is(':has(.necro-post)') || !prev.length) { if (post.is(':has(.necro-post)') || !prev.length) {
return; return;
@ -350,27 +351,34 @@ define('forum/topic/posts', [
} }
var diff = post.attr('data-timestamp') - prev.attr('data-timestamp'); var diff = post.attr('data-timestamp') - prev.attr('data-timestamp');
if (Math.abs(diff) >= necroThreshold) { return new Promise(function (resolve) {
var suffixAgo = $.timeago.settings.strings.suffixAgo; if (Math.abs(diff) >= necroThreshold) {
var prefixAgo = $.timeago.settings.strings.prefixAgo; var suffixAgo = $.timeago.settings.strings.suffixAgo;
var suffixFromNow = $.timeago.settings.strings.suffixFromNow; var prefixAgo = $.timeago.settings.strings.prefixAgo;
var prefixFromNow = $.timeago.settings.strings.prefixFromNow; var suffixFromNow = $.timeago.settings.strings.suffixFromNow;
var prefixFromNow = $.timeago.settings.strings.prefixFromNow;
$.timeago.settings.strings.suffixAgo = '';
$.timeago.settings.strings.prefixAgo = ''; $.timeago.settings.strings.suffixAgo = '';
$.timeago.settings.strings.suffixFromNow = ''; $.timeago.settings.strings.prefixAgo = '';
$.timeago.settings.strings.prefixFromNow = ''; $.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.suffixAgo = suffixAgo;
$.timeago.settings.strings.suffixFromNow = suffixFromNow; $.timeago.settings.strings.prefixAgo = prefixAgo;
$.timeago.settings.strings.prefixFromNow = prefixFromNow; $.timeago.settings.strings.suffixFromNow = suffixFromNow;
app.parseAndTranslate('partials/topic/necro-post', { text: translationText }, function (html) { $.timeago.settings.strings.prefixFromNow = prefixFromNow;
html.insertBefore(post); 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; 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 // todo: should also use topicCommand, but no write api call exists for this yet
topicContainer.on('click', '[component="topic/mark-unread"]', function () { topicContainer.on('click', '[component="topic/mark-unread"]', function () {
socket.emit('topics.markUnread', tid, function (err) { 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)); 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, '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, '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; return router;
}; };

@ -1,8 +1,10 @@
'use strict'; 'use strict';
const _ = require('lodash');
const db = require('../database'); const db = require('../database');
const user = require('../user'); const user = require('../user');
const posts = require('../posts'); const posts = require('../posts');
const categories = require('../categories');
const plugins = require('../plugins'); const plugins = require('../plugins');
const Events = module.exports; const Events = module.exports;
@ -42,6 +44,10 @@ Events._types = {
icon: 'fa-trash-o', icon: 'fa-trash-o',
text: '[[topic:restored-by]]', text: '[[topic:restored-by]]',
}, },
move: {
icon: 'fa-arrow-circle-right',
// text: '[[topic:moved-from-by]]',
},
'post-queue': { 'post-queue': {
icon: 'fa-history', icon: 'fa-history',
text: '[[topic:queued-by]]', text: '[[topic:queued-by]]',
@ -83,6 +89,12 @@ async function getUserInfo(uids) {
return userMap; 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 }) { async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
// Add posts from post queue // Add posts from post queue
const isPrivileged = await user.isPrivileged(uid); 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) // Remove events whose types no longer exist (e.g. plugin uninstalled)
events = events.filter(event => Events._types.hasOwnProperty(event.type)); 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')) { if (event.hasOwnProperty('uid')) {
event.user = users.get(event.uid === 'system' ? 'system' : parseInt(event.uid, 10)); 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]); Object.assign(event, Events._types[event.type]);
}); });
@ -149,11 +168,19 @@ Events.log = async (tid, payload) => {
return events; return events;
}; };
Events.purge = async (tid) => { Events.purge = async (tid, eventIds = []) => {
// Should only be called on topic purge if (eventIds.length) {
const keys = [`topic:${tid}:events`]; const isTopicEvent = await db.isSortedSetMembers(`topic:${tid}:events`, eventIds);
const eventIds = await db.getSortedSetRange(keys[0], 0, -1); eventIds = eventIds.filter((id, index) => isTopicEvent[index]);
keys.push(...eventIds.map(id => `topicEvent:${id}`)); await Promise.all([
db.sortedSetRemove(`topic:${tid}:events`, eventIds),
await db.deleteAll(keys); 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);
}
}; };

@ -268,6 +268,7 @@ module.exports = function (Topics) {
oldCid: oldCid, oldCid: oldCid,
}), }),
Topics.updateCategoryTagsCount([oldCid, cid], tags), Topics.updateCategoryTagsCount([oldCid, cid], tags),
Topics.events.log(tid, { type: 'move', uid: data.uid, fromCid: oldCid }),
]); ]);
const hookData = _.clone(data); const hookData = _.clone(data);
hookData.fromCid = oldCid; hookData.fromCid = oldCid;

Loading…
Cancel
Save