Category watch state (#7109)
* feat: wip, category watch change * feat: pass data to client * feat: allow changing state * fix: account page categories * fix: show in unread if topic is followed or category is watched * feat: add default watch state to acp * feat: save user category watch state * feat: update unread recent pages * fix: remove dupe code * fix: flip conditions * fix: handle empty arrays * fix: ignore/watch on others profile * feat: upgrade script for category states if there are any users ignoring categories set their state in new zset and delete cid:<cid>:ignorers * fix: upgrade * fix: tests * fix: redis count * fix: more testsv1.18.x
parent
2104877c76
commit
eb7ae54f81
@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
const db = require('../database');
|
||||
const user = require('../user');
|
||||
|
||||
module.exports = function (Categories) {
|
||||
Categories.watchStates = {
|
||||
ignoring: 1,
|
||||
notwatching: 2,
|
||||
watching: 3,
|
||||
};
|
||||
|
||||
Categories.isIgnored = function (cids, uid, callback) {
|
||||
if (!(parseInt(uid, 10) > 0)) {
|
||||
return setImmediate(callback, null, cids.map(() => false));
|
||||
}
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Categories.getWatchState(cids, uid, next);
|
||||
},
|
||||
function (states, next) {
|
||||
next(null, states.map(state => state === Categories.watchStates.ignoring));
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Categories.getWatchState = function (cids, uid, callback) {
|
||||
if (!(parseInt(uid, 10) > 0)) {
|
||||
return setImmediate(callback, null, cids.map(() => Categories.watchStates.notwatching));
|
||||
}
|
||||
if (!Array.isArray(cids) || !cids.length) {
|
||||
return setImmediate(callback, null, []);
|
||||
}
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
const keys = cids.map(cid => 'cid:' + cid + ':uid:watch:state');
|
||||
async.parallel({
|
||||
userSettings: async.apply(user.getSettings, uid),
|
||||
states: async.apply(db.sortedSetsScore, keys, uid),
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
next(null, results.states.map(state => state || Categories.watchStates[results.userSettings.categoryWatchState]));
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Categories.getIgnorers = function (cid, start, stop, callback) {
|
||||
const count = (stop === -1) ? -1 : (stop - start + 1);
|
||||
db.getSortedSetRevRangeByScore('cid:' + cid + ':uid:watch:state', start, count, Categories.watchStates.ignoring, Categories.watchStates.ignoring, callback);
|
||||
};
|
||||
|
||||
Categories.filterIgnoringUids = function (cid, uids, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Categories.getUidsWatchStates(cid, uids, next);
|
||||
},
|
||||
function (states, next) {
|
||||
const readingUids = uids.filter((uid, index) => uid && states[index] !== Categories.watchStates.ignoring);
|
||||
next(null, readingUids);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Categories.getUidsWatchStates = function (cid, uids, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
userSettings: async.apply(user.getMultipleUserSettings, uids),
|
||||
states: async.apply(db.sortedSetScores, 'cid:' + cid + ':uid:watch:state', uids),
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
next(null, results.states.map((state, index) => state || Categories.watchStates[results.userSettings[index].categoryWatchState]));
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
var db = require('../../database');
|
||||
var batch = require('../../batch');
|
||||
var categories = require('../../categories');
|
||||
|
||||
module.exports = {
|
||||
name: 'Update category watch data',
|
||||
timestamp: Date.UTC(2018, 11, 13),
|
||||
method: function (callback) {
|
||||
const progress = this.progress;
|
||||
let keys;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRange('categories:cids', 0, -1, next);
|
||||
},
|
||||
function (cids, next) {
|
||||
keys = cids.map(cid => 'cid:' + cid + ':ignorers');
|
||||
batch.processSortedSet('users:joindate', function (uids, next) {
|
||||
progress.incr(uids.length);
|
||||
|
||||
async.eachSeries(cids, function (cid, next) {
|
||||
db.isSortedSetMembers('cid:' + cid + ':ignorers', uids, function (err, isMembers) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
uids = uids.filter((uid, index) => isMembers[index]);
|
||||
if (!uids.length) {
|
||||
return setImmediate(next);
|
||||
}
|
||||
const states = uids.map(() => categories.watchStates.ignoring);
|
||||
db.sortedSetAdd('cid:' + cid + ':uid:watch:state', states, uids, next);
|
||||
});
|
||||
}, next);
|
||||
}, {
|
||||
progress: progress,
|
||||
}, next);
|
||||
},
|
||||
function (next) {
|
||||
db.deleteAll(keys, next);
|
||||
},
|
||||
], callback);
|
||||
},
|
||||
};
|
@ -1,85 +1,89 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
|
||||
var db = require('../database');
|
||||
var categories = require('../categories');
|
||||
const db = require('../database');
|
||||
const categories = require('../categories');
|
||||
|
||||
module.exports = function (User) {
|
||||
User.getIgnoredCategories = function (uid, callback) {
|
||||
if (parseInt(uid, 10) <= 0) {
|
||||
return setImmediate(callback, null, []);
|
||||
User.setCategoryWatchState = function (uid, cid, state, callback) {
|
||||
if (!(parseInt(uid, 10) > 0)) {
|
||||
return setImmediate(callback);
|
||||
}
|
||||
const isStateValid = Object.keys(categories.watchStates).some(key => categories.watchStates[key] === parseInt(state, 10));
|
||||
if (!isStateValid) {
|
||||
return setImmediate(callback, new Error('[[error:invalid-watch-state]]'));
|
||||
}
|
||||
let cids;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
categories.getAllCidsFromSet('categories:cid', next);
|
||||
},
|
||||
function (_cids, next) {
|
||||
cids = _cids;
|
||||
db.isMemberOfSortedSets(cids.map(cid => 'cid:' + cid + ':ignorers'), uid, next);
|
||||
categories.exists(cid, next);
|
||||
},
|
||||
function (isMembers, next) {
|
||||
next(null, cids.filter((cid, index) => isMembers[index]));
|
||||
function (exists, next) {
|
||||
if (!exists) {
|
||||
return next(new Error('[[error:no-category]]'));
|
||||
}
|
||||
|
||||
db.sortedSetAdd('cid:' + cid + ':uid:watch:state', state, uid, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
User.getWatchedCategories = function (uid, callback) {
|
||||
User.getCategoryWatchState = function (uid, callback) {
|
||||
if (parseInt(uid, 10) <= 0) {
|
||||
return setImmediate(callback, null, {});
|
||||
}
|
||||
|
||||
let cids;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
ignored: function (next) {
|
||||
User.getIgnoredCategories(uid, next);
|
||||
},
|
||||
all: function (next) {
|
||||
categories.getAllCidsFromSet('categories:cid', next);
|
||||
},
|
||||
}, next);
|
||||
function (_cids, next) {
|
||||
cids = _cids;
|
||||
categories.getWatchState(cids, uid, next);
|
||||
},
|
||||
function (results, next) {
|
||||
const ignored = new Set(results.ignored);
|
||||
const watched = results.all.filter(cid => cid && !ignored.has(String(cid)));
|
||||
next(null, watched);
|
||||
function (states, next) {
|
||||
next(null, _.zipObject(cids, states));
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
User.ignoreCategory = function (uid, cid, callback) {
|
||||
if (uid <= 0) {
|
||||
return setImmediate(callback);
|
||||
User.getIgnoredCategories = function (uid, callback) {
|
||||
if (parseInt(uid, 10) <= 0) {
|
||||
return setImmediate(callback, null, []);
|
||||
}
|
||||
User.getCategoriesByStates(uid, [categories.watchStates.ignoring], callback);
|
||||
};
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
categories.exists(cid, next);
|
||||
},
|
||||
function (exists, next) {
|
||||
if (!exists) {
|
||||
return next(new Error('[[error:no-category]]'));
|
||||
User.getWatchedCategories = function (uid, callback) {
|
||||
if (parseInt(uid, 10) <= 0) {
|
||||
return setImmediate(callback, null, []);
|
||||
}
|
||||
|
||||
db.sortedSetAdd('cid:' + cid + ':ignorers', Date.now(), uid, next);
|
||||
},
|
||||
], callback);
|
||||
User.getCategoriesByStates(uid, [categories.watchStates.watching], callback);
|
||||
};
|
||||
|
||||
User.watchCategory = function (uid, cid, callback) {
|
||||
if (uid <= 0) {
|
||||
return callback();
|
||||
User.getCategoriesByStates = function (uid, states, callback) {
|
||||
if (!(parseInt(uid, 10) > 0)) {
|
||||
return categories.getAllCidsFromSet('categories:cid', callback);
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
categories.exists(cid, next);
|
||||
User.getCategoryWatchState(uid, next);
|
||||
},
|
||||
function (exists, next) {
|
||||
if (!exists) {
|
||||
return next(new Error('[[error:no-category]]'));
|
||||
}
|
||||
|
||||
db.sortedSetRemove('cid:' + cid + ':ignorers', uid, next);
|
||||
function (userState, next) {
|
||||
const cids = Object.keys(userState);
|
||||
next(null, cids.filter(cid => states.includes(userState[cid])));
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
User.ignoreCategory = function (uid, cid, callback) {
|
||||
User.setCategoryWatchState(uid, cid, categories.watchStates.ignoring, callback);
|
||||
};
|
||||
|
||||
User.watchCategory = function (uid, cid, callback) {
|
||||
User.setCategoryWatchState(uid, cid, categories.watchStates.watching, callback);
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue