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:
Barış Soner Uşaklı
2019-09-26 21:55:49 -04:00
committed by GitHub
parent 17437897f9
commit 310c6fd33f
12 changed files with 171 additions and 193 deletions

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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 () {

View File

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