mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-30 10:35:55 +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
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