mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 19:15:58 +01:00 
			
		
		
		
	properly filter /unread /recent /popular /top (#7927)
* feat: add failing test for pagination * feat: test * fix: redis tests * refactor: remove logs * fix: add new test * feat: make sortedSetRangeByScore work with keys on redis * fix: hardcoded set name * feat: show topics from readable categories on recent/popular/top * feat: rewrite unread topics respect watched categories and followed topics * fix: term + watched
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							17437897f9
						
					
				
				
					commit
					310c6fd33f
				
			| @@ -77,7 +77,7 @@ Categories.getAllCategories = async function (uid) { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| Categories.getCidsByPrivilege = async function (set, uid, privilege) { | Categories.getCidsByPrivilege = async function (set, uid, privilege) { | ||||||
| 	const cids = await Categories.getAllCidsFromSet('categories:cid'); | 	const cids = await Categories.getAllCidsFromSet(set); | ||||||
| 	return await privileges.categories.filterCids(privilege, cids, uid); | 	return await privileges.categories.filterCids(privilege, cids, uid); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -213,7 +213,7 @@ helpers.getCategories = async function (set, uid, privilege, selectedCid) { | |||||||
|  |  | ||||||
| helpers.getCategoriesByStates = async function (uid, selectedCid, states) { | helpers.getCategoriesByStates = async function (uid, selectedCid, states) { | ||||||
| 	let cids = await user.getCategoriesByStates(uid, states); | 	let cids = await user.getCategoriesByStates(uid, states); | ||||||
| 	cids = await privileges.categories.filterCids('read', cids, uid); | 	cids = await privileges.categories.filterCids('topics:read', cids, uid); | ||||||
| 	return await getCategoryData(cids, uid, selectedCid); | 	return await getCategoryData(cids, uid, selectedCid); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ recentController.getData = async function (req, url, sort) { | |||||||
| 	const stop = start + settings.topicsPerPage - 1; | 	const stop = start + settings.topicsPerPage - 1; | ||||||
|  |  | ||||||
| 	const data = await topics.getSortedTopics({ | 	const data = await topics.getSortedTopics({ | ||||||
| 		cids: cid, | 		cids: cid || categoryData.categories.map(c => c.cid), | ||||||
| 		uid: req.uid, | 		uid: req.uid, | ||||||
| 		start: start, | 		start: start, | ||||||
| 		stop: stop, | 		stop: stop, | ||||||
|   | |||||||
| @@ -11,28 +11,28 @@ module.exports = function (module) { | |||||||
| 	require('./sorted/intersect')(module); | 	require('./sorted/intersect')(module); | ||||||
|  |  | ||||||
| 	module.getSortedSetRange = async function (key, start, stop) { | 	module.getSortedSetRange = async function (key, start, stop) { | ||||||
| 		return await sortedSetRange('zrange', key, start, stop, false); | 		return await sortedSetRange('zrange', key, start, stop, '-inf', '+inf', false); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	module.getSortedSetRevRange = async function (key, start, stop) { | 	module.getSortedSetRevRange = async function (key, start, stop) { | ||||||
| 		return await sortedSetRange('zrevrange', key, start, stop, false); | 		return await sortedSetRange('zrevrange', key, start, stop, '-inf', '+inf', false); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	module.getSortedSetRangeWithScores = async function (key, start, stop) { | 	module.getSortedSetRangeWithScores = async function (key, start, stop) { | ||||||
| 		return await sortedSetRange('zrange', key, start, stop, true); | 		return await sortedSetRange('zrange', key, start, stop, '-inf', '+inf', true); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	module.getSortedSetRevRangeWithScores = async function (key, start, stop) { | 	module.getSortedSetRevRangeWithScores = async function (key, start, stop) { | ||||||
| 		return await sortedSetRange('zrevrange', key, start, stop, true); | 		return await sortedSetRange('zrevrange', key, start, stop, '-inf', '+inf', true); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	async function sortedSetRange(method, key, start, stop, withScores) { | 	async function sortedSetRange(method, key, start, stop, min, max, withScores) { | ||||||
| 		if (Array.isArray(key)) { | 		if (Array.isArray(key)) { | ||||||
| 			if (!key.length) { | 			if (!key.length) { | ||||||
| 				return []; | 				return []; | ||||||
| 			} | 			} | ||||||
| 			const batch = module.client.batch(); | 			const batch = module.client.batch(); | ||||||
| 			key.forEach(key => batch[method]([key, 0, stop, 'WITHSCORES'])); | 			key.forEach(key => batch[method](genParams(method, key, 0, stop, min, max, true))); | ||||||
| 			const data = await helpers.execBatch(batch); | 			const data = await helpers.execBatch(batch); | ||||||
|  |  | ||||||
| 			const batchData = data.map(setData => helpers.zsetToObjectArray(setData)); | 			const batchData = data.map(setData => helpers.zsetToObjectArray(setData)); | ||||||
| @@ -48,11 +48,7 @@ module.exports = function (module) { | |||||||
| 			return objects; | 			return objects; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var params = [key, start, stop]; | 		const params = genParams(method, key, start, stop, min, max, withScores); | ||||||
| 		if (withScores) { |  | ||||||
| 			params.push('WITHSCORES'); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		const data = await module.client.async[method](params); | 		const data = await module.client.async[method](params); | ||||||
| 		if (!withScores) { | 		if (!withScores) { | ||||||
| 			return data; | 			return data; | ||||||
| @@ -61,25 +57,46 @@ module.exports = function (module) { | |||||||
| 		return objects; | 		return objects; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	function genParams(method, key, start, stop, min, max, withScores) { | ||||||
|  | 		const params = { | ||||||
|  | 			zrevrange: [key, start, stop], | ||||||
|  | 			zrange: [key, start, stop], | ||||||
|  | 			zrangebyscore: [key, min, max], | ||||||
|  | 			zrevrangebyscore: [key, max, min], | ||||||
|  | 		}; | ||||||
|  | 		if (withScores) { | ||||||
|  | 			params[method].push('WITHSCORES'); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (method === 'zrangebyscore' || method === 'zrevrangebyscore') { | ||||||
|  | 			const count = stop !== -1 ? stop - start + 1 : stop; | ||||||
|  | 			params[method].push('LIMIT', start, count); | ||||||
|  | 		} | ||||||
|  | 		return params[method]; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	module.getSortedSetRangeByScore = async function (key, start, count, min, max) { | 	module.getSortedSetRangeByScore = async function (key, start, count, min, max) { | ||||||
| 		return await module.client.async.zrangebyscore([key, min, max, 'LIMIT', start, count]); | 		return await sortedSetRangeByScore('zrangebyscore', key, start, count, min, max, false); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { | 	module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { | ||||||
| 		return await module.client.async.zrevrangebyscore([key, max, min, 'LIMIT', start, count]); | 		return await sortedSetRangeByScore('zrevrangebyscore', key, start, count, min, max, false); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { | 	module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { | ||||||
| 		return await sortedSetRangeByScoreWithScores('zrangebyscore', key, start, count, min, max); | 		return await sortedSetRangeByScore('zrangebyscore', key, start, count, min, max, true); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { | 	module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { | ||||||
| 		return await sortedSetRangeByScoreWithScores('zrevrangebyscore', key, start, count, max, min); | 		return await sortedSetRangeByScore('zrevrangebyscore', key, start, count, min, max, true); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	async function sortedSetRangeByScoreWithScores(method, key, start, count, min, max) { | 	async function sortedSetRangeByScore(method, key, start, count, min, max, withScores) { | ||||||
| 		const data = await module.client.async[method]([key, min, max, 'WITHSCORES', 'LIMIT', start, count]); | 		if (parseInt(count, 10) === 0) { | ||||||
| 		return helpers.zsetToObjectArray(data); | 			return []; | ||||||
|  | 		} | ||||||
|  | 		const stop = (parseInt(count, 10) === -1) ? -1 : (start + count - 1); | ||||||
|  | 		return await sortedSetRange(method, key, start, stop, min, max, withScores); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	module.sortedSetCount = async function (key, min, max) { | 	module.sortedSetCount = async function (key, min, max) { | ||||||
|   | |||||||
| @@ -103,9 +103,7 @@ module.exports = function (privileges) { | |||||||
|  |  | ||||||
| 		cids = _.uniq(cids); | 		cids = _.uniq(cids); | ||||||
| 		const results = await privileges.categories.getBase(privilege, cids, uid); | 		const results = await privileges.categories.getBase(privilege, cids, uid); | ||||||
| 		return cids.filter(function (cid, index) { | 		return cids.filter((cid, index) => !!cid && !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin)); | ||||||
| 			return !!cid && !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin); |  | ||||||
| 		}); |  | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	privileges.categories.getBase = async function (privilege, cids, uid) { | 	privileges.categories.getBase = async function (privilege, cids, uid) { | ||||||
|   | |||||||
| @@ -71,10 +71,7 @@ module.exports = function (privileges) { | |||||||
| 		let cids = _.uniq(topicsData.map(topic => topic.cid)); | 		let cids = _.uniq(topicsData.map(topic => topic.cid)); | ||||||
| 		const results = await privileges.categories.getBase(privilege, cids, uid); | 		const results = await privileges.categories.getBase(privilege, cids, uid); | ||||||
|  |  | ||||||
| 		cids = cids.filter(function (cid, index) { | 		cids = cids.filter((cid, index) => !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin)); | ||||||
| 			return !results.categories[index].disabled && |  | ||||||
| 				(results.allowedTo[index] || results.isAdmin); |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		const cidsSet = new Set(cids); | 		const cidsSet = new Set(cids); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
|  |  | ||||||
|  | const db = require('../../database'); | ||||||
| const user = require('../../user'); | const user = require('../../user'); | ||||||
| const topics = require('../../topics'); | const topics = require('../../topics'); | ||||||
|  |  | ||||||
| @@ -53,7 +54,7 @@ module.exports = function (SocketTopics) { | |||||||
| 			throw new Error('[[error:no-privileges]]'); | 			throw new Error('[[error:no-privileges]]'); | ||||||
| 		} | 		} | ||||||
| 		const isAdmin = await user.isAdministrator(socket.uid); | 		const isAdmin = await user.isAdministrator(socket.uid); | ||||||
|  | 		const now = Date.now(); | ||||||
| 		await Promise.all(tids.map(async (tid) => { | 		await Promise.all(tids.map(async (tid) => { | ||||||
| 			const topicData = await topics.getTopicFields(tid, ['tid', 'cid']); | 			const topicData = await topics.getTopicFields(tid, ['tid', 'cid']); | ||||||
| 			if (!topicData.tid) { | 			if (!topicData.tid) { | ||||||
| @@ -64,7 +65,8 @@ module.exports = function (SocketTopics) { | |||||||
| 				throw new Error('[[error:no-privileges]]'); | 				throw new Error('[[error:no-privileges]]'); | ||||||
| 			} | 			} | ||||||
| 			await topics.markAsUnreadForAll(tid); | 			await topics.markAsUnreadForAll(tid); | ||||||
| 			await topics.updateRecent(tid, Date.now()); | 			await topics.updateRecent(tid, now); | ||||||
|  | 			await db.sortedSetAdd('cid:' + topicData.cid + ':tids:lastposttime', now, tid); | ||||||
| 		})); | 		})); | ||||||
| 		topics.pushUnreadCount(socket.uid); | 		topics.pushUnreadCount(socket.uid); | ||||||
| 	}; | 	}; | ||||||
|   | |||||||
| @@ -134,8 +134,7 @@ module.exports = function (Topics) { | |||||||
| 			return []; | 			return []; | ||||||
| 		} | 		} | ||||||
| 		const scores = await db.sortedSetScores('uid:' + uid + ':followed_tids', tids); | 		const scores = await db.sortedSetScores('uid:' + uid + ':followed_tids', tids); | ||||||
| 		tids = tids.filter((tid, index) => tid && !!scores[index]); | 		return tids.filter((tid, index) => tid && !!scores[index]); | ||||||
| 		return tids; |  | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	Topics.filterNotIgnoredTids = async function (tids, uid) { | 	Topics.filterNotIgnoredTids = async function (tids, uid) { | ||||||
| @@ -143,8 +142,7 @@ module.exports = function (Topics) { | |||||||
| 			return tids; | 			return tids; | ||||||
| 		} | 		} | ||||||
| 		const scores = await db.sortedSetScores('uid:' + uid + ':ignored_tids', tids); | 		const scores = await db.sortedSetScores('uid:' + uid + ':ignored_tids', tids); | ||||||
| 		tids = tids.filter((tid, index) => tid && !scores[index]); | 		return tids.filter((tid, index) => tid && !scores[index]); | ||||||
| 		return tids; |  | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	Topics.notifyFollowers = async function (postData, exceptUid) { | 	Topics.notifyFollowers = async function (postData, exceptUid) { | ||||||
|   | |||||||
| @@ -33,29 +33,32 @@ module.exports = function (Topics) { | |||||||
|  |  | ||||||
| 	async function getTids(params) { | 	async function getTids(params) { | ||||||
| 		let tids = []; | 		let tids = []; | ||||||
| 		if (params.term === 'alltime') { | 		if (params.term !== 'alltime') { | ||||||
| 			if (params.cids) { | 			tids = await Topics.getLatestTidsFromSet('topics:tid', 0, -1, params.term); | ||||||
| 				tids = await getCidTids(params.cids, params.sort); | 			if (params.filter === 'watched') { | ||||||
|  | 				tids = await Topics.filterWatchedTids(tids, params.uid); | ||||||
|  | 			} | ||||||
|  | 		} else if (params.filter === 'watched') { | ||||||
|  | 			tids = await db.getSortedSetRevRange('uid:' + params.uid + ':followed_tids', 0, -1); | ||||||
|  | 		} else if (params.cids) { | ||||||
|  | 			tids = await getCidTids(params); | ||||||
| 		} else { | 		} else { | ||||||
| 			tids = await db.getSortedSetRevRange('topics:' + params.sort, 0, 199); | 			tids = await db.getSortedSetRevRange('topics:' + params.sort, 0, 199); | ||||||
| 		} | 		} | ||||||
| 		} else { | 		if (params.term !== 'alltime' || params.cids || params.filter === 'watched' || params.floatPinned) { | ||||||
| 			tids = await Topics.getLatestTidsFromSet('topics:tid', 0, -1, params.term); |  | ||||||
| 		} |  | ||||||
| 		if (params.term !== 'alltime' || params.cids || params.floatPinned) { |  | ||||||
| 			tids = await sortTids(tids, params); | 			tids = await sortTids(tids, params); | ||||||
| 		} | 		} | ||||||
| 		return await filterTids(tids, params); | 		return await filterTids(tids.slice(0, 200), params); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	async function getCidTids(cids, sort) { | 	async function getCidTids(params) { | ||||||
| 		const sets = []; | 		const sets = []; | ||||||
| 		const pinnedSets = []; | 		const pinnedSets = []; | ||||||
| 		cids.forEach(function (cid) { | 		params.cids.forEach(function (cid) { | ||||||
| 			if (sort === 'recent') { | 			if (params.sort === 'recent') { | ||||||
| 				sets.push('cid:' + cid + ':tids'); | 				sets.push('cid:' + cid + ':tids'); | ||||||
| 			} else { | 			} else { | ||||||
| 				sets.push('cid:' + cid + ':tids' + (sort ? ':' + sort : '')); | 				sets.push('cid:' + cid + ':tids' + (params.sort ? ':' + params.sort : '')); | ||||||
| 			} | 			} | ||||||
| 			pinnedSets.push('cid:' + cid + ':tids:pinned'); | 			pinnedSets.push('cid:' + cid + ':tids:pinned'); | ||||||
| 		}); | 		}); | ||||||
| @@ -115,9 +118,7 @@ module.exports = function (Topics) { | |||||||
| 		const filter = params.filter; | 		const filter = params.filter; | ||||||
| 		const uid = params.uid; | 		const uid = params.uid; | ||||||
|  |  | ||||||
| 		if (filter === 'watched') { | 		if (filter === 'new') { | ||||||
| 			tids = await Topics.filterWatchedTids(tids, uid); |  | ||||||
| 		} else if (filter === 'new') { |  | ||||||
| 			tids = await Topics.filterNewTids(tids, uid); | 			tids = await Topics.filterNewTids(tids, uid); | ||||||
| 		} else if (filter === 'unreplied') { | 		} else if (filter === 'unreplied') { | ||||||
| 			tids = await Topics.filterUnrepliedTids(tids); | 			tids = await Topics.filterUnrepliedTids(tids); | ||||||
| @@ -130,7 +131,7 @@ module.exports = function (Topics) { | |||||||
| 		const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); | 		const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); | ||||||
|  |  | ||||||
| 		async function getIgnoredCids() { | 		async function getIgnoredCids() { | ||||||
| 			if (filter === 'watched' || meta.config.disableRecentCategoryFilter) { | 			if (params.cids || filter === 'watched' || meta.config.disableRecentCategoryFilter) { | ||||||
| 				return []; | 				return []; | ||||||
| 			} | 			} | ||||||
| 			return await categories.isIgnored(topicCids, uid); | 			return await categories.isIgnored(topicCids, uid); | ||||||
| @@ -144,9 +145,7 @@ module.exports = function (Topics) { | |||||||
| 		topicData = filtered; | 		topicData = filtered; | ||||||
|  |  | ||||||
| 		const cids = params.cids && params.cids.map(String); | 		const cids = params.cids && params.cids.map(String); | ||||||
| 		tids = topicData.filter(function (topic) { | 		tids = topicData.filter(t => t && t.cid && !isCidIgnored[t.cid] && (!cids || cids.includes(String(t.cid)))).map(t => t.tid); | ||||||
| 			return topic && topic.cid && !isCidIgnored[topic.cid] && (!cids || (cids.length && cids.includes(topic.cid.toString()))); |  | ||||||
| 		}).map(topic => topic.tid); |  | ||||||
|  |  | ||||||
| 		const result = await plugins.fireHook('filter:topics.filterSortedTids', { tids: tids, params: params }); | 		const result = await plugins.fireHook('filter:topics.filterSortedTids', { tids: tids, params: params }); | ||||||
| 		return result.tids; | 		return result.tids; | ||||||
|   | |||||||
| @@ -1,18 +1,18 @@ | |||||||
|  |  | ||||||
| 'use strict'; | 'use strict'; | ||||||
|  |  | ||||||
| var async = require('async'); | const async = require('async'); | ||||||
| var _ = require('lodash'); | const _ = require('lodash'); | ||||||
|  |  | ||||||
| var db = require('../database'); | const db = require('../database'); | ||||||
| var user = require('../user'); | const user = require('../user'); | ||||||
| var posts = require('../posts'); | const posts = require('../posts'); | ||||||
| var notifications = require('../notifications'); | const notifications = require('../notifications'); | ||||||
| var categories = require('../categories'); | const categories = require('../categories'); | ||||||
| var privileges = require('../privileges'); | const privileges = require('../privileges'); | ||||||
| var meta = require('../meta'); | const meta = require('../meta'); | ||||||
| var utils = require('../utils'); | const utils = require('../utils'); | ||||||
| var plugins = require('../plugins'); | const plugins = require('../plugins'); | ||||||
|  |  | ||||||
| module.exports = function (Topics) { | module.exports = function (Topics) { | ||||||
| 	Topics.getTotalUnread = async function (uid, filter) { | 	Topics.getTotalUnread = async function (uid, filter) { | ||||||
| @@ -22,7 +22,7 @@ module.exports = function (Topics) { | |||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	Topics.getUnreadTopics = async function (params) { | 	Topics.getUnreadTopics = async function (params) { | ||||||
| 		var unreadTopics = { | 		const unreadTopics = { | ||||||
| 			showSelect: true, | 			showSelect: true, | ||||||
| 			nextStart: 0, | 			nextStart: 0, | ||||||
| 			topics: [], | 			topics: [], | ||||||
| @@ -57,51 +57,18 @@ module.exports = function (Topics) { | |||||||
|  |  | ||||||
| 	Topics.getUnreadData = async function (params) { | 	Topics.getUnreadData = async function (params) { | ||||||
| 		const uid = parseInt(params.uid, 10); | 		const uid = parseInt(params.uid, 10); | ||||||
| 		const counts = { |  | ||||||
| 			'': 0, |  | ||||||
| 			new: 0, |  | ||||||
| 			watched: 0, |  | ||||||
| 			unreplied: 0, |  | ||||||
| 		}; |  | ||||||
| 		const noUnreadData = { |  | ||||||
| 			tids: [], |  | ||||||
| 			counts: counts, |  | ||||||
| 			tidsByFilter: { |  | ||||||
| 				'': [], |  | ||||||
| 				new: [], |  | ||||||
| 				watched: [], |  | ||||||
| 				unreplied: [], |  | ||||||
| 			}, |  | ||||||
| 		}; |  | ||||||
|  |  | ||||||
| 		if (uid <= 0) { |  | ||||||
| 			return noUnreadData; |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		params.filter = params.filter || ''; | 		params.filter = params.filter || ''; | ||||||
|  |  | ||||||
| 		var cutoff = params.cutoff || Topics.unreadCutoff(); |  | ||||||
|  |  | ||||||
| 		if (params.cid && !Array.isArray(params.cid)) { | 		if (params.cid && !Array.isArray(params.cid)) { | ||||||
| 			params.cid = [params.cid]; | 			params.cid = [params.cid]; | ||||||
| 		} | 		} | ||||||
| 		const [ignoredTids, recentTids, userScores, tids_unread] = await Promise.all([ |  | ||||||
| 			user.getIgnoredTids(uid, 0, -1), |  | ||||||
| 			db.getSortedSetRevRangeByScoreWithScores('topics:recent', 0, -1, '+inf', cutoff), |  | ||||||
| 			db.getSortedSetRevRangeByScoreWithScores('uid:' + uid + ':tids_read', 0, -1, '+inf', cutoff), |  | ||||||
| 			db.getSortedSetRevRangeWithScores('uid:' + uid + ':tids_unread', 0, -1), |  | ||||||
| 		]); |  | ||||||
|  |  | ||||||
| 		if (recentTids && !recentTids.length && !tids_unread.length) { | 		const data = await getTids(params); | ||||||
| 			return noUnreadData; | 		if (!data.tids && !data.tids.length) { | ||||||
|  | 			return data; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		const data = await filterTopics(params, { |  | ||||||
| 			ignoredTids: ignoredTids, |  | ||||||
| 			recentTids: recentTids, |  | ||||||
| 			userScores: userScores, |  | ||||||
| 			tids_unread: tids_unread, |  | ||||||
| 		}); |  | ||||||
| 		const result = await plugins.fireHook('filter:topics.getUnreadTids', { | 		const result = await plugins.fireHook('filter:topics.getUnreadTids', { | ||||||
| 			uid: uid, | 			uid: uid, | ||||||
| 			tids: data.tids, | 			tids: data.tids, | ||||||
| @@ -113,83 +80,69 @@ module.exports = function (Topics) { | |||||||
| 		return result; | 		return result; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	async function filterTopics(params, results) { | 	async function getTids(params) { | ||||||
| 		const counts = { | 		const counts = { '': 0,	new: 0,	watched: 0,	unreplied: 0 }; | ||||||
| 			'': 0, | 		const tidsByFilter = { '': [], new: [],	watched: [], unreplied: [] }; | ||||||
| 			new: 0, |  | ||||||
| 			watched: 0, |  | ||||||
| 			unreplied: 0, |  | ||||||
| 		}; |  | ||||||
|  |  | ||||||
| 		const tidsByFilter = { | 		if (params.uid <= 0) { | ||||||
| 			'': [], | 			return { counts: counts, tids: [], tidsByFilter: tidsByFilter }; | ||||||
| 			new: [], |  | ||||||
| 			watched: [], |  | ||||||
| 			unreplied: [], |  | ||||||
| 		}; |  | ||||||
|  |  | ||||||
| 		var userRead = {}; |  | ||||||
| 		results.userScores.forEach(function (userItem) { |  | ||||||
| 			userRead[userItem.value] = userItem.score; |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		results.recentTids = results.recentTids.concat(results.tids_unread); |  | ||||||
| 		results.recentTids.sort(function (a, b) { |  | ||||||
| 			return b.score - a.score; |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		var tids = results.recentTids.filter(function (recentTopic) { |  | ||||||
| 			if (results.ignoredTids.includes(String(recentTopic.value))) { |  | ||||||
| 				return false; |  | ||||||
| 		} | 		} | ||||||
| 			return !userRead[recentTopic.value] || recentTopic.score > userRead[recentTopic.value]; |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		tids = _.uniq(tids.map(topic => topic.value)); | 		const cutoff = params.cutoff || Topics.unreadCutoff(); | ||||||
|  |  | ||||||
| 		var cid = params.cid; | 		const [followedTids, ignoredTids, recentTids, userScores, tids_unread] = await Promise.all([ | ||||||
| 		var uid = params.uid; | 			getFollowedTids(params), | ||||||
| 		var cids; | 			user.getIgnoredTids(params.uid, 0, -1), | ||||||
| 		var topicData; | 			getRecentTids(params), | ||||||
|  | 			db.getSortedSetRevRangeByScoreWithScores('uid:' + params.uid + ':tids_read', 0, -1, '+inf', cutoff), | ||||||
|  | 			db.getSortedSetRevRangeWithScores('uid:' + params.uid + ':tids_unread', 0, -1), | ||||||
|  | 		]); | ||||||
|  |  | ||||||
| 		tids = tids.slice(0, 200); | 		const userReadTime = _.mapValues(_.keyBy(userScores, 'value'), 'score'); | ||||||
|  | 		const isTopicsFollowed = _.mapValues(_.keyBy(followedTids, 'value'), 'score'); | ||||||
|  |  | ||||||
|  | 		const unreadTopics = _.unionWith(recentTids, followedTids.concat(tids_unread), (a, b) => a.value === b.value) | ||||||
|  | 			.filter(t => !ignoredTids.includes(t.value) && (!userReadTime[t.value] || t.score > userReadTime[t.value])) | ||||||
|  | 			.sort((a, b) => b.score - a.score); | ||||||
|  |  | ||||||
|  | 		let tids = _.uniq(unreadTopics.map(topic => topic.value)).slice(0, 200); | ||||||
|  |  | ||||||
| 		if (!tids.length) { | 		if (!tids.length) { | ||||||
| 			return { counts: counts, tids: tids, tidsByFilter: tidsByFilter }; | 			return { counts: counts, tids: tids, tidsByFilter: tidsByFilter }; | ||||||
| 		} | 		} | ||||||
| 		const blockedUids = await user.blocks.list(uid); |  | ||||||
|  | 		const blockedUids = await user.blocks.list(params.uid); | ||||||
|  |  | ||||||
| 		tids = await filterTidsThatHaveBlockedPosts({ | 		tids = await filterTidsThatHaveBlockedPosts({ | ||||||
| 			uid: uid, | 			uid: params.uid, | ||||||
| 			tids: tids, | 			tids: tids, | ||||||
| 			blockedUids: blockedUids, | 			blockedUids: blockedUids, | ||||||
| 			recentTids: results.recentTids, | 			recentTids: recentTids, | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount']); | 		const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount']); | ||||||
| 		cids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); | 		const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); | ||||||
|  |  | ||||||
| 		const [isTopicsFollowed, categoryWatchState, readCids] = await Promise.all([ | 		const [categoryWatchState, readCids] = await Promise.all([ | ||||||
| 			db.sortedSetScores('uid:' + uid + ':followed_tids', tids), | 			categories.getWatchState(topicCids, params.uid), | ||||||
| 			categories.getWatchState(cids, uid), | 			privileges.categories.filterCids('topics:read', topicCids, params.uid), | ||||||
| 			privileges.categories.filterCids('read', cids, uid), |  | ||||||
| 		]); | 		]); | ||||||
| 		cid = cid && cid.map(String); |  | ||||||
| 		const readableCids = readCids.map(String); |  | ||||||
| 		const userCidState = _.zipObject(cids, categoryWatchState); |  | ||||||
|  |  | ||||||
| 		topicData.forEach(function (topic, index) { | 		const filterCids = params.cid && params.cid.map(String); | ||||||
| 			function cidMatch(topicCid) { | 		const readableCids = readCids.map(String); | ||||||
| 				return (!cid || (cid.length && cid.includes(String(topicCid)))) && readableCids.includes(String(topicCid)); | 		const userCidState = _.zipObject(topicCids, categoryWatchState); | ||||||
|  |  | ||||||
|  | 		topicData.forEach(function (topic) { | ||||||
|  | 			function cidMatch() { | ||||||
|  | 				return (!filterCids || (filterCids.length && filterCids.includes(String(topic.cid)))) && readableCids.includes(String(topic.cid)); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if (topic && topic.cid && cidMatch(topic.cid) && !blockedUids.includes(parseInt(topic.uid, 10))) { | 			if (topic && topic.cid && cidMatch() && !blockedUids.includes(topic.uid)) { | ||||||
| 				topic.tid = parseInt(topic.tid, 10); | 				if (isTopicsFollowed[topic.tid] || userCidState[topic.cid] === categories.watchStates.watching) { | ||||||
| 				if ((isTopicsFollowed[index] || userCidState[topic.cid] === categories.watchStates.watching)) { |  | ||||||
| 					tidsByFilter[''].push(topic.tid); | 					tidsByFilter[''].push(topic.tid); | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if (isTopicsFollowed[index]) { | 				if (isTopicsFollowed[topic.tid]) { | ||||||
| 					tidsByFilter.watched.push(topic.tid); | 					tidsByFilter.watched.push(topic.tid); | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| @@ -197,11 +150,12 @@ module.exports = function (Topics) { | |||||||
| 					tidsByFilter.unreplied.push(topic.tid); | 					tidsByFilter.unreplied.push(topic.tid); | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if (!userRead[topic.tid]) { | 				if (!userReadTime[topic.tid]) { | ||||||
| 					tidsByFilter.new.push(topic.tid); | 					tidsByFilter.new.push(topic.tid); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		counts[''] = tidsByFilter[''].length; | 		counts[''] = tidsByFilter[''].length; | ||||||
| 		counts.watched = tidsByFilter.watched.length; | 		counts.watched = tidsByFilter.watched.length; | ||||||
| 		counts.unreplied = tidsByFilter.unreplied.length; | 		counts.unreplied = tidsByFilter.unreplied.length; | ||||||
| @@ -214,6 +168,25 @@ module.exports = function (Topics) { | |||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	async function getRecentTids(params) { | ||||||
|  | 		if (params.filter === 'watched') { | ||||||
|  | 			return []; | ||||||
|  | 		} | ||||||
|  | 		const cutoff = params.cutoff || Topics.unreadCutoff(); | ||||||
|  | 		const cids = params.cid || await user.getWatchedCategories(params.uid); | ||||||
|  | 		const keys = cids.map(cid => 'cid:' + cid + ':tids:lastposttime'); | ||||||
|  | 		return await db.getSortedSetRevRangeByScoreWithScores(keys, 0, -1, '+inf', cutoff); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	async function getFollowedTids(params) { | ||||||
|  | 		const tids = await db.getSortedSetRevRange('uid:' + params.uid + ':followed_tids', 0, -1); | ||||||
|  | 		const scores = await db.sortedSetScores('topics:recent', tids); | ||||||
|  | 		const cutoff = params.cutoff || Topics.unreadCutoff(); | ||||||
|  |  | ||||||
|  | 		const data = tids.map((tid, index) => ({ value: tid, score: scores[index] })); | ||||||
|  | 		return data.filter(item => item.score > cutoff); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	async function filterTidsThatHaveBlockedPosts(params) { | 	async function filterTidsThatHaveBlockedPosts(params) { | ||||||
| 		if (!params.blockedUids.length) { | 		if (!params.blockedUids.length) { | ||||||
| 			return params.tids; | 			return params.tids; | ||||||
| @@ -234,14 +207,14 @@ module.exports = function (Topics) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	async function doesTidHaveUnblockedUnreadPosts(tid, params) { | 	async function doesTidHaveUnblockedUnreadPosts(tid, params) { | ||||||
| 		var userLastReadTimestamp = params.userLastReadTimestamp; | 		const userLastReadTimestamp = params.userLastReadTimestamp; | ||||||
| 		if (!userLastReadTimestamp) { | 		if (!userLastReadTimestamp) { | ||||||
| 			return true; | 			return true; | ||||||
| 		} | 		} | ||||||
| 		var start = 0; | 		let start = 0; | ||||||
| 		var count = 3; | 		const count = 3; | ||||||
| 		var done = false; | 		let done = false; | ||||||
| 		var hasUnblockedUnread = params.topicTimestamp > userLastReadTimestamp; | 		let hasUnblockedUnread = params.topicTimestamp > userLastReadTimestamp; | ||||||
| 		if (!params.blockedUids.length) { | 		if (!params.blockedUids.length) { | ||||||
| 			return hasUnblockedUnread; | 			return hasUnblockedUnread; | ||||||
| 		} | 		} | ||||||
| @@ -252,9 +225,7 @@ module.exports = function (Topics) { | |||||||
| 				return hasUnblockedUnread; | 				return hasUnblockedUnread; | ||||||
| 			} | 			} | ||||||
| 			let postData = await posts.getPostsFields(pidsSinceLastVisit, ['pid', 'uid']); | 			let postData = await posts.getPostsFields(pidsSinceLastVisit, ['pid', 'uid']); | ||||||
| 			postData = postData.filter(function (post) { | 			postData = postData.filter(post => !params.blockedUids.includes(parseInt(post.uid, 10))); | ||||||
| 				return !params.blockedUids.includes(parseInt(post.uid, 10)); |  | ||||||
| 			}); |  | ||||||
|  |  | ||||||
| 			done = postData.length > 0; | 			done = postData.length > 0; | ||||||
| 			hasUnblockedUnread = postData.length > 0; | 			hasUnblockedUnread = postData.length > 0; | ||||||
| @@ -295,23 +266,21 @@ module.exports = function (Topics) { | |||||||
| 			db.sortedSetScores('uid:' + uid + ':tids_read', tids), | 			db.sortedSetScores('uid:' + uid + ':tids_read', tids), | ||||||
| 		]); | 		]); | ||||||
|  |  | ||||||
| 		tids = tids.filter(function (tid, index) { | 		tids = tids.filter((tid, index) => topicScores[index] && (!userScores[index] || userScores[index] < topicScores[index])); | ||||||
| 			return topicScores[index] && (!userScores[index] || userScores[index] < topicScores[index]); |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		if (!tids.length) { | 		if (!tids.length) { | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var now = Date.now(); | 		const now = Date.now(); | ||||||
| 		var scores = tids.map(() => now); | 		const scores = tids.map(() => now); | ||||||
| 		const [topicData] = await Promise.all([ | 		const [topicData] = await Promise.all([ | ||||||
| 			Topics.getTopicsFields(tids, ['cid']), | 			Topics.getTopicsFields(tids, ['cid']), | ||||||
| 			db.sortedSetAdd('uid:' + uid + ':tids_read', scores, tids), | 			db.sortedSetAdd('uid:' + uid + ':tids_read', scores, tids), | ||||||
| 			db.sortedSetRemove('uid:' + uid + ':tids_unread', tids), | 			db.sortedSetRemove('uid:' + uid + ':tids_unread', tids), | ||||||
| 		]); | 		]); | ||||||
|  |  | ||||||
| 		var cids = _.uniq(topicData.map(t => t && t.cid).filter(Boolean)); | 		const cids = _.uniq(topicData.map(t => t && t.cid).filter(Boolean)); | ||||||
| 		await categories.markAsRead(cids, uid); | 		await categories.markAsRead(cids, uid); | ||||||
|  |  | ||||||
| 		plugins.fireHook('action:topics.markAsRead', { uid: uid, tids: tids }); | 		plugins.fireHook('action:topics.markAsRead', { uid: uid, tids: tids }); | ||||||
| @@ -350,9 +319,9 @@ module.exports = function (Topics) { | |||||||
| 			user.blocks.list(uid), | 			user.blocks.list(uid), | ||||||
| 		]); | 		]); | ||||||
|  |  | ||||||
| 		var cutoff = Topics.unreadCutoff(); | 		const cutoff = Topics.unreadCutoff(); | ||||||
| 		var result = tids.map(function (tid, index) { | 		const result = tids.map(function (tid, index) { | ||||||
| 			var read = !tids_unread[index] && | 			const read = !tids_unread[index] && | ||||||
| 				(topicScores[index] < cutoff || | 				(topicScores[index] < cutoff || | ||||||
| 				!!(userScores[index] && userScores[index] >= topicScores[index])); | 				!!(userScores[index] && userScores[index] >= topicScores[index])); | ||||||
| 			return { tid: tid, read: read, index: index }; | 			return { tid: tid, read: read, index: index }; | ||||||
|   | |||||||
| @@ -227,22 +227,11 @@ describe('Sorted Set methods', function () { | |||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		it('should return duplicates if two sets have same elements', function (done) { | 		it('should return duplicates if two sets have same elements', async function () { | ||||||
| 			async.waterfall([ | 			await db.sortedSetAdd('dupezset1', [1, 2], ['value 1', 'value 2']); | ||||||
| 				function (next) { | 			await db.sortedSetAdd('dupezset2', [2, 3], ['value 2', 'value 3']); | ||||||
| 					db.sortedSetAdd('dupezset1', [1, 2], ['value 1', 'value 2'], next); | 			const data = await db.getSortedSetRange(['dupezset1', 'dupezset2'], 0, -1); | ||||||
| 				}, |  | ||||||
| 				function (next) { |  | ||||||
| 					db.sortedSetAdd('dupezset2', [2, 3], ['value 2', 'value 3'], next); |  | ||||||
| 				}, |  | ||||||
| 				function (next) { |  | ||||||
| 					db.getSortedSetRange(['dupezset1', 'dupezset2'], 0, -1, next); |  | ||||||
| 				}, |  | ||||||
| 				function (data, next) { |  | ||||||
| 			assert.deepStrictEqual(data, ['value 1', 'value 2', 'value 2', 'value 3']); | 			assert.deepStrictEqual(data, ['value 1', 'value 2', 'value 2', 'value 3']); | ||||||
| 					next(); |  | ||||||
| 				}, |  | ||||||
| 			], done); |  | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		it('should return correct number of elements', async function () { | 		it('should return correct number of elements', async function () { | ||||||
| @@ -405,6 +394,15 @@ describe('Sorted Set methods', function () { | |||||||
| 				done(); | 				done(); | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | 		it('should work with an array of keys', async function () { | ||||||
|  | 			await db.sortedSetAddBulk([ | ||||||
|  | 				['byScoreWithScoresKeys1', 1, 'value1'], | ||||||
|  | 				['byScoreWithScoresKeys2', 2, 'value2'], | ||||||
|  | 			]); | ||||||
|  | 			const data = await db.getSortedSetRevRangeByScoreWithScores(['byScoreWithScoresKeys1', 'byScoreWithScoresKeys2'], 0, -1, 5, -5); | ||||||
|  | 			assert.deepStrictEqual(data, [{ value: 'value2', score: 2 }, { value: 'value1', score: 1 }]); | ||||||
|  | 		}); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	describe('sortedSetCount()', function () { | 	describe('sortedSetCount()', function () { | ||||||
|   | |||||||
| @@ -1190,14 +1190,14 @@ describe('Topic\'s', function () { | |||||||
| 				topic: function (next) { | 				topic: function (next) { | ||||||
| 					topics.post({ uid: topic.userId, title: 'unread topic', content: 'unread topic content', cid: topic.categoryId }, next); | 					topics.post({ uid: topic.userId, title: 'unread topic', content: 'unread topic content', cid: topic.categoryId }, next); | ||||||
| 				}, | 				}, | ||||||
| 				user: function (next) { | 				joeUid: function (next) { | ||||||
| 					User.create({ username: 'regularJoe' }, next); | 					User.create({ username: 'regularJoe' }, next); | ||||||
| 				}, | 				}, | ||||||
| 			}, function (err, results) { | 			}, function (err, results) { | ||||||
| 				assert.ifError(err); | 				assert.ifError(err); | ||||||
| 				tid = results.topic.topicData.tid; | 				tid = results.topic.topicData.tid; | ||||||
| 				mainPid = results.topic.postData.pid; | 				mainPid = results.topic.postData.pid; | ||||||
| 				uid = results.user; | 				uid = results.joeUid; | ||||||
| 				done(); | 				done(); | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
| @@ -1385,7 +1385,7 @@ describe('Topic\'s', function () { | |||||||
| 				}, | 				}, | ||||||
| 				function (category, next) { | 				function (category, next) { | ||||||
| 					privateCid = category.cid; | 					privateCid = category.cid; | ||||||
| 					privileges.categories.rescind(['read'], category.cid, 'registered-users', next); | 					privileges.categories.rescind(['topics:read'], category.cid, 'registered-users', next); | ||||||
| 				}, | 				}, | ||||||
| 				function (next) { | 				function (next) { | ||||||
| 					topics.post({ uid: adminUid, title: 'topic in private category', content: 'registered-users cant see this', cid: privateCid }, next); | 					topics.post({ uid: adminUid, title: 'topic in private category', content: 'registered-users cant see this', cid: privateCid }, next); | ||||||
| @@ -1414,7 +1414,7 @@ describe('Topic\'s', function () { | |||||||
| 				}, | 				}, | ||||||
| 				function (category, next) { | 				function (category, next) { | ||||||
| 					ignoredCid = category.cid; | 					ignoredCid = category.cid; | ||||||
| 					privileges.categories.rescind(['read'], category.cid, 'registered-users', next); | 					privileges.categories.rescind(['topics:read'], category.cid, 'registered-users', next); | ||||||
| 				}, | 				}, | ||||||
| 				function (next) { | 				function (next) { | ||||||
| 					topics.post({ uid: adminUid, title: 'topic in private category', content: 'registered-users cant see this', cid: ignoredCid }, next); | 					topics.post({ uid: adminUid, title: 'topic in private category', content: 'registered-users cant see this', cid: ignoredCid }, next); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user