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",
"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:

@ -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;
$.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) + ']]';
$.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);
});
}
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 = '';
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);
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}`));
await db.deleteAll(keys);
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);
}
};

@ -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;

Loading…
Cancel
Save