mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 02:55:58 +01:00 
			
		
		
		
	feat(topic-events): work in progress topic events logic and client-side implementation
This commit is contained in:
		| @@ -211,5 +211,7 @@ | ||||
| 	"no-connection": "There seems to be a problem with your internet connection", | ||||
| 	"socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", | ||||
|  | ||||
| 	"plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" | ||||
| 	"plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", | ||||
|  | ||||
| 	"topic-event-unrecognized": "Topic event '%1' unrecognized" | ||||
| } | ||||
|   | ||||
| @@ -22,8 +22,10 @@ | ||||
| 	"login-to-view": "🔒 Log in to view", | ||||
| 	"edit": "Edit", | ||||
| 	"delete": "Delete", | ||||
| 	"deleted": "Deleted", | ||||
| 	"purge": "Purge", | ||||
| 	"restore": "Restore", | ||||
| 	"restored": "Restored", | ||||
| 	"move": "Move", | ||||
| 	"change-owner": "Change Owner", | ||||
| 	"fork": "Fork", | ||||
| @@ -31,8 +33,10 @@ | ||||
| 	"share": "Share", | ||||
| 	"tools": "Tools", | ||||
| 	"locked": "Locked", | ||||
| 	"unlocked": "Unlocked", | ||||
| 	"pinned": "Pinned", | ||||
| 	"pinned-with-expiry": "Pinned until %1", | ||||
| 	"unpinned": "Unpinned", | ||||
| 	"moved": "Moved", | ||||
| 	"moved-from": "Moved from %1", | ||||
| 	"copy-ip": "Copy IP", | ||||
|   | ||||
| @@ -242,6 +242,19 @@ get: | ||||
|                         flagId: | ||||
|                           type: number | ||||
|                           description: The flag identifier, if this particular post has been flagged before | ||||
|                   events: | ||||
|                     type: array | ||||
|                     items: | ||||
|                       type: object | ||||
|                       properties: | ||||
|                         type: | ||||
|                           type: string | ||||
|                         id: | ||||
|                           type: number | ||||
|                         timestamp: | ||||
|                           type: number | ||||
|                         timestampISO: | ||||
|                           type: string | ||||
|                   category: | ||||
|                     $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject | ||||
|                   tagWhitelist: | ||||
|   | ||||
| @@ -271,9 +271,38 @@ define('forum/topic/posts', [ | ||||
| 		posts.find('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); | ||||
| 		Posts.addBlockquoteEllipses(posts); | ||||
| 		hidePostToolsForDeletedPosts(posts); | ||||
| 		addTopicEvents(); | ||||
| 		addNecroPostMessage(); | ||||
| 	}; | ||||
|  | ||||
| 	function addTopicEvents() { | ||||
| 		if (config.topicPostSort !== 'newest_to_oldest' && config.topicPostSort !== 'oldest_to_newest') { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// TODO: Handle oldest_to_newest | ||||
| 		const postTimestamps = ajaxify.data.posts.map(post => post.timestamp); | ||||
| 		ajaxify.data.events.forEach((event) => { | ||||
| 			const beforeIdx = postTimestamps.findIndex(timestamp => timestamp > event.timestamp); | ||||
| 			let postEl; | ||||
| 			if (beforeIdx > -1) { | ||||
| 				postEl = document.querySelector(`[component="post"][data-pid="${ajaxify.data.posts[beforeIdx].pid}"]`); | ||||
| 			} | ||||
|  | ||||
| 			app.parseAndTranslate('partials/topic/event', event, function (html) { | ||||
| 				html = html.get(0); | ||||
|  | ||||
| 				if (postEl) { | ||||
| 					document.querySelector('[component="topic"]').insertBefore(html, postEl); | ||||
| 				} else { | ||||
| 					document.querySelector('[component="topic"]').append(html); | ||||
| 				} | ||||
|  | ||||
| 				$(html).find('.timeago').timeago(); | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	function addNecroPostMessage() { | ||||
| 		var necroThreshold = ajaxify.data.necroThreshold * 24 * 60 * 60 * 1000; | ||||
| 		if (!necroThreshold || (config.topicPostSort !== 'newest_to_oldest' && config.topicPostSort !== 'oldest_to_newest')) { | ||||
|   | ||||
| @@ -63,6 +63,12 @@ exports.doTopicAction = async function (action, event, caller, { tids }) { | ||||
| }; | ||||
|  | ||||
| async function logTopicAction(action, req, tid, title) { | ||||
| 	// No 'purge' topic event (since topic is now gone) | ||||
| 	if (action !== 'purge') { | ||||
| 		await topics.events.log(tid, { type: action }); | ||||
| 	} | ||||
|  | ||||
| 	// Only log certain actions to system event log | ||||
| 	var actionsToLog = ['delete', 'restore', 'purge']; | ||||
| 	if (!actionsToLog.includes(action)) { | ||||
| 		return; | ||||
|   | ||||
							
								
								
									
										90
									
								
								src/topics/events.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/topics/events.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const db = require('../database'); | ||||
| const plugins = require('../plugins'); | ||||
|  | ||||
| const Events = module.exports; | ||||
|  | ||||
| Events._types = { | ||||
| 	pin: { | ||||
| 		icon: 'fa-thumb-tack', | ||||
| 		text: '[[topic:pinned]]', | ||||
| 	}, | ||||
| 	pin_expiry: { | ||||
| 		icon: 'fa-thumb-tack', | ||||
| 		text: '[[topic:pinned-with-expiry]]', | ||||
| 	}, | ||||
| 	unpin: { | ||||
| 		icon: 'fa-thumb-tack', | ||||
| 		text: '[[topic:unpinned]]', | ||||
| 	}, | ||||
| 	lock: { | ||||
| 		icon: 'fa-lock', | ||||
| 		text: '[[topic:locked]]', | ||||
| 	}, | ||||
| 	unlock: { | ||||
| 		icon: 'fa-unlock', | ||||
| 		text: '[[topic:unlocked]]', | ||||
| 	}, | ||||
| 	delete: { | ||||
| 		icon: 'fa-trash', | ||||
| 		text: '[[topic:deleted]]', | ||||
| 	}, | ||||
| 	restore: { | ||||
| 		icon: 'fa-trash-o', | ||||
| 		text: '[[topic:restored]]', | ||||
| 	}, | ||||
| }; | ||||
| Events._ready = false; | ||||
|  | ||||
| Events.init = async () => { | ||||
| 	if (!Events._ready) { | ||||
| 		// Allow plugins to define additional topic event types | ||||
| 		const { types } = await plugins.hooks.fire('filter:topicEvents.init', { types: Events._types }); | ||||
| 		Events._types = types; | ||||
| 		Events._ready = true; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| Events.get = async (tid) => { | ||||
| 	await Events.init(); | ||||
| 	const topics = require('.'); | ||||
|  | ||||
| 	if (!await topics.exists(tid)) { | ||||
| 		throw new Error('[[error:no-topic]]'); | ||||
| 	} | ||||
|  | ||||
| 	const eventIds = await db.getSortedSetRangeWithScores(`topic:${tid}:events`, 0, -1); | ||||
| 	const keys = eventIds.map(obj => `topicEvent:${obj.value}`); | ||||
| 	const timestamps = eventIds.map(obj => obj.score); | ||||
| 	const events = await db.getObjects(keys); | ||||
| 	events.forEach((event, idx) => { | ||||
| 		event.id = parseInt(eventIds[idx].value, 10); | ||||
| 		event.timestamp = timestamps[idx]; | ||||
| 		event.timestampISO = new Date(timestamps[idx]).toISOString(); | ||||
|  | ||||
| 		Object.assign(event, Events._types[event.type]); | ||||
| 	}); | ||||
|  | ||||
| 	return events; | ||||
| }; | ||||
|  | ||||
| Events.log = async (tid, payload) => { | ||||
| 	await Events.init(); | ||||
| 	const topics = require('.'); | ||||
| 	const { type } = payload; | ||||
| 	const now = Date.now(); | ||||
|  | ||||
| 	if (!Events._types.hasOwnProperty(type)) { | ||||
| 		throw new Error(`[[error:topic-event-unrecognized, ${type}]]`); | ||||
| 	} else if (!await topics.exists(tid)) { | ||||
| 		throw new Error('[[error:no-topic]]'); | ||||
| 	} | ||||
|  | ||||
| 	const eventId = await db.incrObjectField('global', 'nextTopicEventId'); | ||||
|  | ||||
| 	await Promise.all([ | ||||
| 		db.setObject(`topicEvent:${eventId}`, payload), | ||||
| 		db.sortedSetAdd(`topic:${tid}:events`, now, eventId), | ||||
| 	]); | ||||
| }; | ||||
| @@ -33,6 +33,7 @@ require('./tools')(Topics); | ||||
| Topics.thumbs = require('./thumbs'); | ||||
| require('./bookmarks')(Topics); | ||||
| require('./merge')(Topics); | ||||
| Topics.events = require('./events'); | ||||
|  | ||||
| Topics.exists = async function (tid) { | ||||
| 	return await db.exists('topic:' + tid); | ||||
| @@ -171,6 +172,7 @@ Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, rev | ||||
| 		merger, | ||||
| 		related, | ||||
| 		thumbs, | ||||
| 		events, | ||||
| 	] = await Promise.all([ | ||||
| 		getMainPostAndReplies(topicData, set, uid, start, stop, reverse), | ||||
| 		categories.getCategoryData(topicData.cid), | ||||
| @@ -183,11 +185,13 @@ Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, rev | ||||
| 		getMerger(topicData), | ||||
| 		getRelated(topicData, uid), | ||||
| 		Topics.thumbs.get(topicData.tid), | ||||
| 		Topics.events.get(topicData.tid), | ||||
| 	]); | ||||
|  | ||||
| 	topicData.thumbs = thumbs; | ||||
| 	restoreThumbValue(topicData); | ||||
| 	topicData.posts = posts; | ||||
| 	topicData.events = events; | ||||
| 	topicData.category = category; | ||||
| 	topicData.tagWhitelist = tagWhitelist[0]; | ||||
| 	topicData.minTags = category.minTags; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user