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

@@ -1,18 +1,18 @@
'use strict';
var async = require('async');
var _ = require('lodash');
const async = require('async');
const _ = require('lodash');
var db = require('../database');
var user = require('../user');
var posts = require('../posts');
var notifications = require('../notifications');
var categories = require('../categories');
var privileges = require('../privileges');
var meta = require('../meta');
var utils = require('../utils');
var plugins = require('../plugins');
const db = require('../database');
const user = require('../user');
const posts = require('../posts');
const notifications = require('../notifications');
const categories = require('../categories');
const privileges = require('../privileges');
const meta = require('../meta');
const utils = require('../utils');
const plugins = require('../plugins');
module.exports = function (Topics) {
Topics.getTotalUnread = async function (uid, filter) {
@@ -22,7 +22,7 @@ module.exports = function (Topics) {
};
Topics.getUnreadTopics = async function (params) {
var unreadTopics = {
const unreadTopics = {
showSelect: true,
nextStart: 0,
topics: [],
@@ -57,51 +57,18 @@ module.exports = function (Topics) {
Topics.getUnreadData = async function (params) {
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 || '';
var cutoff = params.cutoff || Topics.unreadCutoff();
if (params.cid && !Array.isArray(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) {
return noUnreadData;
const data = await getTids(params);
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', {
uid: uid,
tids: data.tids,
@@ -113,83 +80,69 @@ module.exports = function (Topics) {
return result;
};
async function filterTopics(params, results) {
const counts = {
'': 0,
new: 0,
watched: 0,
unreplied: 0,
};
async function getTids(params) {
const counts = { '': 0, new: 0, watched: 0, unreplied: 0 };
const tidsByFilter = { '': [], new: [], watched: [], unreplied: [] };
const tidsByFilter = {
'': [],
new: [],
watched: [],
unreplied: [],
};
if (params.uid <= 0) {
return { counts: counts, tids: [], tidsByFilter: tidsByFilter };
}
var userRead = {};
results.userScores.forEach(function (userItem) {
userRead[userItem.value] = userItem.score;
});
const cutoff = params.cutoff || Topics.unreadCutoff();
results.recentTids = results.recentTids.concat(results.tids_unread);
results.recentTids.sort(function (a, b) {
return b.score - a.score;
});
const [followedTids, ignoredTids, recentTids, userScores, tids_unread] = await Promise.all([
getFollowedTids(params),
user.getIgnoredTids(params.uid, 0, -1),
getRecentTids(params),
db.getSortedSetRevRangeByScoreWithScores('uid:' + params.uid + ':tids_read', 0, -1, '+inf', cutoff),
db.getSortedSetRevRangeWithScores('uid:' + params.uid + ':tids_unread', 0, -1),
]);
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];
});
const userReadTime = _.mapValues(_.keyBy(userScores, 'value'), 'score');
const isTopicsFollowed = _.mapValues(_.keyBy(followedTids, 'value'), 'score');
tids = _.uniq(tids.map(topic => topic.value));
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);
var cid = params.cid;
var uid = params.uid;
var cids;
var topicData;
tids = tids.slice(0, 200);
let tids = _.uniq(unreadTopics.map(topic => topic.value)).slice(0, 200);
if (!tids.length) {
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({
uid: uid,
uid: params.uid,
tids: tids,
blockedUids: blockedUids,
recentTids: results.recentTids,
recentTids: recentTids,
});
topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount']);
cids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean);
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount']);
const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean);
const [isTopicsFollowed, categoryWatchState, readCids] = await Promise.all([
db.sortedSetScores('uid:' + uid + ':followed_tids', tids),
categories.getWatchState(cids, uid),
privileges.categories.filterCids('read', cids, uid),
const [categoryWatchState, readCids] = await Promise.all([
categories.getWatchState(topicCids, params.uid),
privileges.categories.filterCids('topics:read', topicCids, params.uid),
]);
cid = cid && cid.map(String);
const readableCids = readCids.map(String);
const userCidState = _.zipObject(cids, categoryWatchState);
topicData.forEach(function (topic, index) {
function cidMatch(topicCid) {
return (!cid || (cid.length && cid.includes(String(topicCid)))) && readableCids.includes(String(topicCid));
const filterCids = params.cid && params.cid.map(String);
const readableCids = readCids.map(String);
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))) {
topic.tid = parseInt(topic.tid, 10);
if ((isTopicsFollowed[index] || userCidState[topic.cid] === categories.watchStates.watching)) {
if (topic && topic.cid && cidMatch() && !blockedUids.includes(topic.uid)) {
if (isTopicsFollowed[topic.tid] || userCidState[topic.cid] === categories.watchStates.watching) {
tidsByFilter[''].push(topic.tid);
}
if (isTopicsFollowed[index]) {
if (isTopicsFollowed[topic.tid]) {
tidsByFilter.watched.push(topic.tid);
}
@@ -197,11 +150,12 @@ module.exports = function (Topics) {
tidsByFilter.unreplied.push(topic.tid);
}
if (!userRead[topic.tid]) {
if (!userReadTime[topic.tid]) {
tidsByFilter.new.push(topic.tid);
}
}
});
counts[''] = tidsByFilter[''].length;
counts.watched = tidsByFilter.watched.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) {
if (!params.blockedUids.length) {
return params.tids;
@@ -234,14 +207,14 @@ module.exports = function (Topics) {
}
async function doesTidHaveUnblockedUnreadPosts(tid, params) {
var userLastReadTimestamp = params.userLastReadTimestamp;
const userLastReadTimestamp = params.userLastReadTimestamp;
if (!userLastReadTimestamp) {
return true;
}
var start = 0;
var count = 3;
var done = false;
var hasUnblockedUnread = params.topicTimestamp > userLastReadTimestamp;
let start = 0;
const count = 3;
let done = false;
let hasUnblockedUnread = params.topicTimestamp > userLastReadTimestamp;
if (!params.blockedUids.length) {
return hasUnblockedUnread;
}
@@ -252,9 +225,7 @@ module.exports = function (Topics) {
return hasUnblockedUnread;
}
let postData = await posts.getPostsFields(pidsSinceLastVisit, ['pid', 'uid']);
postData = postData.filter(function (post) {
return !params.blockedUids.includes(parseInt(post.uid, 10));
});
postData = postData.filter(post => !params.blockedUids.includes(parseInt(post.uid, 10)));
done = postData.length > 0;
hasUnblockedUnread = postData.length > 0;
@@ -295,23 +266,21 @@ module.exports = function (Topics) {
db.sortedSetScores('uid:' + uid + ':tids_read', tids),
]);
tids = tids.filter(function (tid, index) {
return topicScores[index] && (!userScores[index] || userScores[index] < topicScores[index]);
});
tids = tids.filter((tid, index) => topicScores[index] && (!userScores[index] || userScores[index] < topicScores[index]));
if (!tids.length) {
return false;
}
var now = Date.now();
var scores = tids.map(() => now);
const now = Date.now();
const scores = tids.map(() => now);
const [topicData] = await Promise.all([
Topics.getTopicsFields(tids, ['cid']),
db.sortedSetAdd('uid:' + uid + ':tids_read', scores, 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);
plugins.fireHook('action:topics.markAsRead', { uid: uid, tids: tids });
@@ -350,9 +319,9 @@ module.exports = function (Topics) {
user.blocks.list(uid),
]);
var cutoff = Topics.unreadCutoff();
var result = tids.map(function (tid, index) {
var read = !tids_unread[index] &&
const cutoff = Topics.unreadCutoff();
const result = tids.map(function (tid, index) {
const read = !tids_unread[index] &&
(topicScores[index] < cutoff ||
!!(userScores[index] && userScores[index] >= topicScores[index]));
return { tid: tid, read: read, index: index };