mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 19:15: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", | 	"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", | 	"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", | 	"login-to-view": "🔒 Log in to view", | ||||||
| 	"edit": "Edit", | 	"edit": "Edit", | ||||||
| 	"delete": "Delete", | 	"delete": "Delete", | ||||||
|  | 	"deleted": "Deleted", | ||||||
| 	"purge": "Purge", | 	"purge": "Purge", | ||||||
| 	"restore": "Restore", | 	"restore": "Restore", | ||||||
|  | 	"restored": "Restored", | ||||||
| 	"move": "Move", | 	"move": "Move", | ||||||
| 	"change-owner": "Change Owner", | 	"change-owner": "Change Owner", | ||||||
| 	"fork": "Fork", | 	"fork": "Fork", | ||||||
| @@ -31,8 +33,10 @@ | |||||||
| 	"share": "Share", | 	"share": "Share", | ||||||
| 	"tools": "Tools", | 	"tools": "Tools", | ||||||
| 	"locked": "Locked", | 	"locked": "Locked", | ||||||
|  | 	"unlocked": "Unlocked", | ||||||
| 	"pinned": "Pinned", | 	"pinned": "Pinned", | ||||||
| 	"pinned-with-expiry": "Pinned until %1", | 	"pinned-with-expiry": "Pinned until %1", | ||||||
|  | 	"unpinned": "Unpinned", | ||||||
| 	"moved": "Moved", | 	"moved": "Moved", | ||||||
| 	"moved-from": "Moved from %1", | 	"moved-from": "Moved from %1", | ||||||
| 	"copy-ip": "Copy IP", | 	"copy-ip": "Copy IP", | ||||||
|   | |||||||
| @@ -242,6 +242,19 @@ get: | |||||||
|                         flagId: |                         flagId: | ||||||
|                           type: number |                           type: number | ||||||
|                           description: The flag identifier, if this particular post has been flagged before |                           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: |                   category: | ||||||
|                     $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject |                     $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject | ||||||
|                   tagWhitelist: |                   tagWhitelist: | ||||||
|   | |||||||
| @@ -271,9 +271,38 @@ 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); | ||||||
|  | 		addTopicEvents(); | ||||||
| 		addNecroPostMessage(); | 		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() { | 	function addNecroPostMessage() { | ||||||
| 		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')) { | ||||||
|   | |||||||
| @@ -63,6 +63,12 @@ exports.doTopicAction = async function (action, event, caller, { tids }) { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| async function logTopicAction(action, req, tid, title) { | 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']; | 	var actionsToLog = ['delete', 'restore', 'purge']; | ||||||
| 	if (!actionsToLog.includes(action)) { | 	if (!actionsToLog.includes(action)) { | ||||||
| 		return; | 		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'); | Topics.thumbs = require('./thumbs'); | ||||||
| require('./bookmarks')(Topics); | require('./bookmarks')(Topics); | ||||||
| require('./merge')(Topics); | require('./merge')(Topics); | ||||||
|  | Topics.events = require('./events'); | ||||||
|  |  | ||||||
| Topics.exists = async function (tid) { | Topics.exists = async function (tid) { | ||||||
| 	return await db.exists('topic:' + tid); | 	return await db.exists('topic:' + tid); | ||||||
| @@ -171,6 +172,7 @@ Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, rev | |||||||
| 		merger, | 		merger, | ||||||
| 		related, | 		related, | ||||||
| 		thumbs, | 		thumbs, | ||||||
|  | 		events, | ||||||
| 	] = await Promise.all([ | 	] = await Promise.all([ | ||||||
| 		getMainPostAndReplies(topicData, set, uid, start, stop, reverse), | 		getMainPostAndReplies(topicData, set, uid, start, stop, reverse), | ||||||
| 		categories.getCategoryData(topicData.cid), | 		categories.getCategoryData(topicData.cid), | ||||||
| @@ -183,11 +185,13 @@ Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, rev | |||||||
| 		getMerger(topicData), | 		getMerger(topicData), | ||||||
| 		getRelated(topicData, uid), | 		getRelated(topicData, uid), | ||||||
| 		Topics.thumbs.get(topicData.tid), | 		Topics.thumbs.get(topicData.tid), | ||||||
|  | 		Topics.events.get(topicData.tid), | ||||||
| 	]); | 	]); | ||||||
|  |  | ||||||
| 	topicData.thumbs = thumbs; | 	topicData.thumbs = thumbs; | ||||||
| 	restoreThumbValue(topicData); | 	restoreThumbValue(topicData); | ||||||
| 	topicData.posts = posts; | 	topicData.posts = posts; | ||||||
|  | 	topicData.events = events; | ||||||
| 	topicData.category = category; | 	topicData.category = category; | ||||||
| 	topicData.tagWhitelist = tagWhitelist[0]; | 	topicData.tagWhitelist = tagWhitelist[0]; | ||||||
| 	topicData.minTags = category.minTags; | 	topicData.minTags = category.minTags; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user