diff --git a/install/package.json b/install/package.json
index 05c004cd08..d5d178740f 100644
--- a/install/package.json
+++ b/install/package.json
@@ -100,8 +100,8 @@
"nodebb-plugin-spam-be-gone": "2.0.4",
"nodebb-rewards-essentials": "0.2.1",
"nodebb-theme-lavender": "7.0.2",
- "nodebb-theme-peace": "2.0.8",
- "nodebb-theme-persona": "13.0.28",
+ "nodebb-theme-peace": "2.0.9",
+ "nodebb-theme-persona": "13.0.30",
"nodebb-widget-essentials": "7.0.2",
"nodemailer": "6.8.0",
"nprogress": "0.2.0",
diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json
index 94a3b5db3f..0e6be457d8 100644
--- a/public/language/en-GB/topic.json
+++ b/public/language/en-GB/topic.json
@@ -43,16 +43,26 @@
"ban-ip": "Ban IP",
"view-history": "Edit History",
- "locked-by": "Locked by",
- "unlocked-by": "Unlocked by",
- "pinned-by": "Pinned by",
- "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 →",
- "backlink": "Referenced by",
- "forked-by": "Forked by",
+ "user-locked-topic-ago": "%1 locked this topic %2",
+ "user-locked-topic-on": "%1 locked this topic on %2",
+ "user-unlocked-topic-ago": "%1 unlocked this topic %2",
+ "user-unlocked-topic-on": "%1 unlocked this topic on %2",
+ "user-pinned-topic-ago": "%1 pinned this topic %2",
+ "user-pinned-topic-on": "%1 pinned this topic on %2",
+ "user-unpinned-topic-ago": "%1 unpinned this topic %2",
+ "user-unpinned-topic-on": "%1 unpinned this topic on %2",
+ "user-deleted-topic-ago": "%1 deleted this topic %2",
+ "user-deleted-topic-on": "%1 deleted this topic on %2",
+ "user-restored-topic-ago": "%1 restored this topic %2",
+ "user-restored-topic-on": "%1 restored this topic on %2",
+ "user-moved-topic-from-ago": "%1 moved this topic from %2 %3",
+ "user-moved-topic-from-on": "%1 moved this topic from %2 on %3",
+ "user-queued-post-ago": "%1 queued post for approval %3",
+ "user-queued-post-on": "%1 queued post for approval on %3",
+ "user-referenced-topic-ago": "%1 referenced this topic %3",
+ "user-referenced-topic-on": "%1 referenced this topic on %3",
+ "user-forked-topic-ago": "%1 forked this topic %3",
+ "user-forked-topic-on": "%1 forked this topic on %3",
"bookmark_instructions" : "Click here to return to the last read post in this thread.",
diff --git a/src/topics/events.js b/src/topics/events.js
index 8e2b8e04e1..95f3d772d3 100644
--- a/src/topics/events.js
+++ b/src/topics/events.js
@@ -1,6 +1,7 @@
'use strict';
const _ = require('lodash');
+const nconf = require('nconf');
const db = require('../database');
const meta = require('../meta');
const user = require('../user');
@@ -9,6 +10,10 @@ const categories = require('../categories');
const plugins = require('../plugins');
const translator = require('../translator');
const privileges = require('../privileges');
+const utils = require('../utils');
+const helpers = require('../helpers');
+
+const relative_path = nconf.get('relative_path');
const Events = module.exports;
@@ -20,49 +25,49 @@ const Events = module.exports;
* You can then log a custom topic event by calling `topics.events.log(tid, { type, uid });`
* `uid` is optional; if you pass in a valid uid in the payload,
* the user avatar/username will be rendered as part of the event text
- *
+ * see https://github.com/NodeBB/nodebb-plugin-question-and-answer/blob/master/library.js#L288-L306
*/
Events._types = {
pin: {
icon: 'fa-thumb-tack',
- text: '[[topic:pinned-by]]',
+ translation: async event => translateSimple(event, 'topic:user-pinned-topic'),
},
unpin: {
icon: 'fa-thumb-tack fa-rotate-90',
- text: '[[topic:unpinned-by]]',
+ translation: async event => translateSimple(event, 'topic:user-unpinned-topic'),
},
lock: {
icon: 'fa-lock',
- text: '[[topic:locked-by]]',
+ translation: async event => translateSimple(event, 'topic:user-locked-topic'),
},
unlock: {
icon: 'fa-unlock',
- text: '[[topic:unlocked-by]]',
+ translation: async event => translateSimple(event, 'topic:user-unlocked-topic'),
},
delete: {
icon: 'fa-trash',
- text: '[[topic:deleted-by]]',
+ translation: async event => translateSimple(event, 'topic:user-deleted-topic'),
},
restore: {
icon: 'fa-trash-o',
- text: '[[topic:restored-by]]',
+ translation: async event => translateSimple(event, 'topic:user-restored-topic'),
},
move: {
icon: 'fa-arrow-circle-right',
- // text: '[[topic:moved-from-by]]',
+ translation: async event => translateEventArgs(event, 'topic:user-moved-topic-from', renderUser(event), `${event.fromCategory.name}`, renderTimeago(event)),
},
'post-queue': {
icon: 'fa-history',
- text: '[[topic:queued-by]]',
href: '/post-queue',
+ translation: async event => translateEventArgs(event, 'topic:user-queued-post', renderUser(event), `/post-queue`, renderTimeago(event)),
},
backlink: {
icon: 'fa-link',
- text: '[[topic:backlink]]',
+ translation: async event => translateEventArgs(event, 'topic:user-referenced-topic', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
},
fork: {
icon: 'fa-code-fork',
- text: '[[topic:forked-by]]',
+ translation: async event => translateEventArgs(event, 'topic:user-forked-topic', renderUser(event), `${relative_path}${event.href}`, renderTimeago(event)),
},
};
@@ -72,6 +77,40 @@ Events.init = async () => {
Events._types = types;
};
+async function translateEventArgs(event, prefix, ...args) {
+ const key = getTranslationKey(event, prefix);
+ const compiled = translator.compile.apply(null, [key, ...args]);
+ return utils.decodeHTMLEntities(await translator.translate(compiled));
+}
+
+async function translateSimple(event, prefix) {
+ return await translateEventArgs(event, prefix, renderUser(event), renderTimeago(event));
+}
+
+Events.translateSimple = translateSimple; // so plugins can perform translate
+Events.translateEventArgs = translateEventArgs; // so plugins can perform translate
+
+// generate `user-locked-topic-ago` or `user-locked-topic-on` based on timeago cutoff setting
+function getTranslationKey(event, prefix) {
+ const cutoffMs = 1000 * 60 * 60 * 24 * Math.max(0, parseInt(meta.config.timeagoCutoff, 10));
+ let translationSuffix = 'ago';
+ if (cutoffMs > 0 && Date.now() - event.timestamp > cutoffMs) {
+ translationSuffix = 'on';
+ }
+ return `${prefix}-${translationSuffix}`;
+}
+
+function renderUser(event) {
+ if (!event.user || event.user.system) {
+ return '[[global:system-user]]';
+ }
+ return `${helpers.buildAvatar(event.user, '16px', true)} ${event.user.username}`;
+}
+
+function renderTimeago(event) {
+ return ``;
+}
+
Events.get = async (tid, uid, reverse = false) => {
const topics = require('.');
@@ -154,12 +193,17 @@ async function modifyEvent({ tid, uid, eventIds, timestamps, events }) {
}
if (event.hasOwnProperty('fromCid')) {
event.fromCategory = fromCategories[event.fromCid];
- event.text = translator.compile('topic:moved-from-by', event.fromCategory.name);
}
Object.assign(event, Events._types[event.type]);
});
+ await Promise.all(events.map(async (event) => {
+ if (Events._types[event.type].translation) {
+ event.text = await Events._types[event.type].translation(event);
+ }
+ }));
+
// Sort events
events.sort((a, b) => a.timestamp - b.timestamp);