diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index aa46be739a..3a5afe053d 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -49,7 +49,7 @@ define('notifications', ['sounds'], function(sound) { updateNotifCount(data.unread.length); - socket.emit('modules.notifications.mark_all_read', null, function(err) { + socket.emit('modules.notifications.markAllRead', null, function(err) { if (!err) { updateNotifCount(0); } diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index b0204b9932..86ed0027d9 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -41,6 +41,9 @@ module.exports = function(redisClient, module) { }; module.getObjectsFields = function(keys, fields, callback) { + if (!Array.isArray(fields) || !fields.length) { + return callback(null, keys.map(function() { return {}; })); + } var multi = redisClient.multi(); for(var x=0; x= parseInt(oldNotifObj.importance, 10)); + }); + }); + }); + } + + function hasNotification(uniqueId, uid, callback) { + async.parallel([ + async.apply(db.isSortedSetMember, 'uid:' + uid + ':notifications:unread', uniqueId), + async.apply(db.isSortedSetMember, 'uid:' + uid + ':notifications:read', uniqueId) + ], function(err, results) { + if (err) { + return callback(err); + } + + callback(null, results[0] || results[1]); + }); + } + Notifications.pushGroup = function(nid, groupName, callback) { if (!callback) { callback = function() {}; @@ -182,129 +215,55 @@ var async = require('async'), }); }; - function checkReplace(uniqueId, uid, newNotifObj, callback) { - var replace = false, matched = false; - function checkAndRemove(set, next) { - db.getSortedSetRange(set, 0, -1, function(err, nids) { - if (err || !nids || !nids.length) { - return next(err); - } - - var keys = nids.map(function(nid) { - return 'notifications:' + nid; - }); - - db.getObjectsFields(keys, ['nid', 'uniqueId', 'importance'], function(err, nid_infos) { - if (err) { - return next(err); - } - - nid_infos.forEach(function(nid_info) { - if (nid_info && nid_info.uniqueId === uniqueId) { - matched = true; - if ((nid_info.importance || 5) >= newNotifObj.importance) { - replace = true; - db.sortedSetRemove(set, nid_info.nid); - } - } - - }); - - next(); - }); - }); - } - - async.parallel([ - function(next) { - checkAndRemove('uid:' + uid + ':notifications:unread', next); - }, - function(next) { - checkAndRemove('uid:' + uid + ':notifications:read', next); - } - ], function(err) { - if (!err) { - if (replace === false && matched === false) { - replace = true; - } - - callback(null, replace); - } - }); - } - - Notifications.mark_read = function(nid, uid, callback) { + Notifications.markRead = function(nid, uid, callback) { callback = callback || function() {}; if (!parseInt(uid, 10)) { return callback(); } - Notifications.get(nid, uid, function(notif_data) { + Notifications.get(nid, function(err, notificationData) { + if (err || !notificationData) { + return callback(err); + } + async.parallel([ - function(next) { - db.sortedSetRemove('uid:' + uid + ':notifications:unread', nid, next); - }, - function(next) { - if (!notif_data) { - return next(); - } - db.sortedSetAdd('uid:' + uid + ':notifications:read', notif_data.datetime, nid, next); - } + async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:unread', notificationData.uniqueId), + async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:read', notificationData.datetime, notificationData.uniqueId) ], callback); }); }; - Notifications.mark_read_multiple = function(nids, uid, callback) { + Notifications.markReadMultiple = function(nids, uid, callback) { + callback = callback || function() {}; if (!Array.isArray(nids) && parseInt(nids, 10) > 0) { nids = [nids]; } async.each(nids, function(nid, next) { - Notifications.mark_read(nid, uid, function(err) { - if (!err) { - next(null); - } - }); - }, function(err) { - if (callback) { - callback(err); - } - }); + Notifications.markRead(nid, uid, next); + }, callback); }; - Notifications.mark_all_read = function(uid, callback) { - db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, 10, function(err, nids) { + Notifications.markAllRead = function(uid, callback) { + db.getObjectValues('uid:' + uid + ':notifications:uniqueId:nid', function(err, nids) { if (err) { return callback(err); } - if (nids.length > 0) { - Notifications.mark_read_multiple(nids, uid, function(err) { - callback(err); - }); - } else { - callback(); + if (!Array.isArray(nids) || !nids.length) { + return callback(err); } + + Notifications.markReadMultiple(nids, uid, callback); }); }; - // why_are_we_using_underscores_here_? - // maybe_camel_case_ALL_THE_THINGS - Notifications.mark_read_by_uniqueid = function(uid, uniqueId, callback) { + Notifications.markReadByUniqueId = function(uid, uniqueId, callback) { async.waterfall([ - async.apply(db.getSortedSetRange, 'uid:' + uid + ':notifications:unread', 0, 10), - function(nids, next) { - async.filter(nids, function(nid, next) { - db.getObjectField('notifications:' + nid, 'uniqueId', function(err, value) { - next(uniqueId === value); - }); - }, function(nids) { - next(null, nids); - }); - }, - function(nids, next) { - Notifications.mark_read_multiple(nids, uid, next); + async.apply(db.getObjectField, 'uid:' + uid + ':notifications:uniqueId:nid', uniqueId), + function(nid, next) { + Notifications.markRead(nid, uid, next); } ], callback); }; @@ -326,13 +285,13 @@ var async = require('async'), var cutoffTime = cutoff.getTime(); db.getSetMembers('notifications', function(err, nids) { + if (err) { + return winston.error(err.message); + } + async.filter(nids, function(nid, next) { db.getObjectField('notifications:' + nid, 'datetime', function(err, datetime) { - if (parseInt(datetime, 10) < cutoffTime) { - next(true); - } else { - next(false); - } + next(!err && parseInt(datetime, 10) < cutoffTime); }); }, function(expiredNids) { async.each(expiredNids, function(nid, next) { diff --git a/src/routes/debug.js b/src/routes/debug.js index a03c5b2649..d41bfac333 100644 --- a/src/routes/debug.js +++ b/src/routes/debug.js @@ -56,6 +56,38 @@ module.exports = function(app, middleware, controllers) { }); router.get('/test', function(req, res) { - res.redirect('404'); + //res.redirect('404'); + var notifications = require('../notifications'); + var nconf = require('nconf'); + + var username = 'julian'; + var topicTitle = 'testing tags'; + var topicSlug = '1748/testing-tags'; + var postIndex = 1; + var tid = 1748; + var fromUid = 2; + + notifications.create({ + bodyShort: '[[notifications:user_posted_to, ' + username + ', ' + topicTitle + ']]', + bodyLong: 'asdasd khajsdhakhdakj hdkash dakhdakjdhakjs', + path: nconf.get('relative_path') + '/topic/' + topicSlug + '/' + postIndex, + uniqueId: 'topic:' + tid, + from: fromUid + }, function(err, nid) { + notifications.push(nid, [1]); + res.json('done'); + }); }); + + router.get('/dailyunread', function(req, res) { + //var userNotifs = require('./user'); + user.notifications.getDailyUnread(1, function(err, data) { + if (err) { + res.json(500, err.message); + } + + res.json(data); + + }); + }) }; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index b396299fa9..45fde242dd 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -231,8 +231,10 @@ function sendChatNotification(fromuid, touid, messageObj) { path: nconf.get('relative_path') + '/chats/' + utils.slugify(messageObj.fromUser.username), uniqueId: 'chat_' + fromuid + '_' + touid, from: fromuid - }, function(nid) { - notifications.push(nid, [touid]); + }, function(err, nid) { + if (!err) { + notifications.push(nid, [touid]); + } }); } } @@ -278,12 +280,12 @@ SocketModules.chats.list = function(socket, data, callback) { }; /* Notifications */ -SocketModules.notifications.mark_read = function(socket, nid) { - notifications.mark_read(nid, socket.uid); +SocketModules.notifications.markRead = function(socket, nid) { + notifications.markRead(nid, socket.uid); }; -SocketModules.notifications.mark_all_read = function(socket, data, callback) { - notifications.mark_all_read(socket.uid, callback); +SocketModules.notifications.markAllRead = function(socket, data, callback) { + notifications.markAllRead(socket.uid, callback); }; /* Sounds */ diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index 3636a1f0a2..7756f2e28e 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -124,8 +124,10 @@ function sendNotificationToPostOwner(data, uid, notification) { path: nconf.get('relative_path') + '/topic/' + results.slug + '/' + results.index, uniqueId: 'post:' + data.pid, from: uid - }, function(nid) { - notifications.push(nid, [postData.uid]); + }, function(err, nid) { + if (!err) { + notifications.push(nid, [postData.uid]); + } }); }); }); @@ -327,10 +329,11 @@ SocketPosts.flag = function(socket, pid, callback) { path: path, uniqueId: 'post_flag:' + pid, from: socket.uid - }, function(nid) { - notifications.push(nid, adminGroup.members, function() { - next(); - }); + }, function(err, nid) { + if (err) { + return next(err); + } + notifications.push(nid, adminGroup.members, next); }); }, function(next) { diff --git a/src/topics/follow.js b/src/topics/follow.js index 644dde717c..ae20fc70bc 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -47,9 +47,7 @@ module.exports = function(Topics) { path: nconf.get('relative_path') + '/topic/' + results.topicData.slug + '/' + results.postIndex, uniqueId: 'topic:' + tid, from: exceptUid - }, function(nid) { - next(null, nid); - }); + }, next); }); }, followers: function(next) { diff --git a/src/topics/unread.js b/src/topics/unread.js index f7a3f8a700..7316bf7759 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -181,8 +181,8 @@ module.exports = function(Topics) { }; Topics.markTopicNotificationsRead = function(tid, uid) { - user.notifications.getUnreadByUniqueId(uid, 'topic:' + tid, function(err, nids) { - notifications.mark_read_multiple(nids, uid, function() { + user.notifications.getUnreadByField(uid, 'tid', tid, function(err, nids) { + notifications.markReadMultiple(nids, uid, function() { user.notifications.pushCount(uid); }); }); diff --git a/src/user/create.js b/src/user/create.js index b0d6850ea5..b3b556edb7 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -163,8 +163,10 @@ module.exports = function(User) { bodyLong: '', image: 'brand:logo', datetime: Date.now() - }, function(nid) { - notifications.push(nid, uid); + }, function(err, nid) { + if (!err) { + notifications.push(nid, uid); + } }); } diff --git a/src/user/delete.js b/src/user/delete.js index 29af39a6ec..8ab14c4cea 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -68,6 +68,9 @@ module.exports = function(User) { function(next) { db.delete('uid:' + uid + ':notifications:unread', next); }, + function(next) { + db.delete('uid:' + uid + ':notifications:uniqueId:nid', next); + }, function(next) { db.sortedSetRemove('users:joindate', uid, next); }, diff --git a/src/user/notifications.js b/src/user/notifications.js index d4f1b79708..e2c782a55f 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -15,30 +15,57 @@ var async = require('async'), privileges = require('../privileges'); (function(UserNotifications) { + UserNotifications.get = function(uid, callback) { + + function getNotifications(set, start, stop, iterator, done) { - db.getSortedSetRevRange(set, start, stop, function(err, nids) { + db.getSortedSetRevRange(set, start, stop, function(err, uniqueIds) { if(err) { return done(err); } - if(!nids || nids.length === 0) { + if(!Array.isArray(uniqueIds) || !uniqueIds.length) { return done(null, []); } - if (nids.length > maxNotifs) { - nids.length = maxNotifs; + if (uniqueIds.length > maxNotifs) { + uniqueIds.length = maxNotifs; } - async.map(nids, function(nid, next) { - notifications.get(nid, uid, function(notif_data) { - if(typeof iterator === 'function') { - iterator(notif_data); - } + db.getObjectFields('uid:' + uid + ':notifications:uniqueId:nid', uniqueIds, function(err, uniqueIdToNids) { + if (err) { + return done(err); + } - next(null, notif_data); + var nidsToUniqueIds = {}; + Object.keys(uniqueIdToNids).forEach(function(uniqueId) { + nidsToUniqueIds[uniqueIdToNids[uniqueId]] = uniqueId; }); - }, done); + + async.map(Object.keys(nidsToUniqueIds), function(nid, next) { + notifications.get(nid, function(err, notif_data) { + if (err) { + return next(err); + } + + if (!notif_data) { + if (process.env.NODE_ENV === 'development') { + winston.info('[notifications.get] nid ' + nid + ' not found. Removing.'); + } + + db.sortedSetRemove(set, nidsToUniqueIds[nid]); + return next(); + } + + if (typeof iterator === 'function') { + iterator(notif_data, next); + } else { + next(null, notif_data); + } + }); + }, done); + }); }); } @@ -47,13 +74,16 @@ var async = require('async'), async.parallel({ unread: function(next) { getNotifications('uid:' + uid + ':notifications:unread', 0, 9, function(notif_data) { - if (notif_data) { - notif_data.readClass = !notif_data.read ? 'label-warning' : ''; - } + notif_data.read = false; + notif_data.readClass = !notif_data.read ? 'label-warning' : ''; + next(null, notif_data); }, next); }, read: function(next) { - getNotifications('uid:' + uid + ':notifications:read', 0, 9, null, next); + getNotifications('uid:' + uid + ':notifications:read', 0, 9, function(notif_data, next) { + notif_data.read = true; + next(null, notif_data); + }, next); } }, function(err, notifications) { function filterDeleted(notifObj) { @@ -85,41 +115,75 @@ var async = require('async'), before = new Date(parseInt(before, 10)); } - db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:read', 0, limit, before ? before.getTime(): now.getTime(), -Infinity, function(err, results1) { - db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, limit, before ? before.getTime(): now.getTime(), -Infinity, function(err, results2) { + db.getObjectValues('uid:' + uid + ':notifications:uniqueId:nid', function(err, nids) { + if (err) { + return callback(err); + } - var nids = results1.concat(results2); - async.map(nids, function(nid, next) { - notifications.get(nid, uid, function(notif_data) { + async.map(nids, function(nid, next) { + notifications.get(nid, function(err, notif_data) { + if (err || !notif_data) { + return next(err); + } + UserNotifications.isNotificationRead(notif_data.nid, uid, function(err, isRead) { + if (err) { + return next(err); + } + + notif_data.read = isRead; next(null, notif_data); }); - }, function(err, notifs) { - notifs = notifs.filter(function(notif) { - return notif !== null; - }).sort(function(a, b) { - return parseInt(b.datetime, 10) - parseInt(a.datetime, 10); - }).map(function(notif) { - notif.datetimeISO = utils.toISOString(notif.datetime); - notif.readClass = !notif.read ? 'label-warning' : ''; - - return notif; - }); - - callback(err, notifs); }); + }, function(err, notifs) { + if (err) { + return callback(err); + } + + notifs = notifs.filter(function(notif) { + return notif !== null; + }).sort(function(a, b) { + return parseInt(b.datetime, 10) - parseInt(a.datetime, 10); + }).map(function(notif) { + notif.datetimeISO = utils.toISOString(notif.datetime); + notif.readClass = !notif.read ? 'label-warning' : ''; + return notif; + }); + + callback(null, notifs); }); }); }; + UserNotifications.isNotificationRead = function(nid, uid, callback) { + db.isSortedSetMember('uid:' + uid + ':notifications:read', nid, callback); + }; + UserNotifications.getDailyUnread = function(uid, callback) { var now = Date.now(), yesterday = now - (1000*60*60*24); // Approximate, can be more or less depending on time changes, makes no difference really. - db.getSortedSetRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, yesterday, now, function(err, nids) { - async.map(nids, function(nid, next) { - notifications.get(nid, uid, function(notif_data) { - next(null, notif_data); + + db.getSortedSetRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, yesterday, now, function(err, uniqueIds) { + if (err) { + return callback(err); + } + + if (!Array.isArray(uniqueIds) || !uniqueIds.length) { + return callback(null, []); + } + + db.getObjectFields('uid:' + uid + ':notifications:uniqueId:nid', uniqueIds, function(err, uniqueIdToNids) { + if (err) { + return callback(err); + } + + var nids = Object.keys(uniqueIdToNids).map(function(uniqueId) { + return uniqueIdToNids[uniqueId]; }); - }, callback); + + async.map(nids, function(nid, next) { + notifications.get(nid, next); + }, callback); + }); }); }; @@ -127,27 +191,41 @@ var async = require('async'), db.sortedSetCount('uid:' + uid + ':notifications:unread', -Infinity, Infinity, callback); }; - UserNotifications.getUnreadByUniqueId = function(uid, uniqueId, callback) { - db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, -1, function(err, nids) { + UserNotifications.getUnreadByField = function(uid, field, value, callback) { + db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, -1, function(err, uniqueIds) { + if (err) { + return callback(err); + } - async.filter(nids, function(nid, next) { - notifications.get(nid, uid, function(notifObj) { - if(!notifObj) { - return next(false); - } + if (!Array.isArray(uniqueIds) || !uniqueIds.length) { + return callback(null, []); + } - if (notifObj.uniqueId === uniqueId) { - next(true); - } else { - next(false); - } + db.getObjectFields('uid:' + uid + ':notifications:uniqueId:nid', uniqueIds, function(err, uniqueIdsToNids) { + if (err) { + return callback(err); + } + + var nids = Object.keys(uniqueIdsToNids).map(function(uniqueId) { + return uniqueIdsToNids[uniqueId]; + }); + + async.filter(nids, function(nid, next) { + notifications.get(nid, uid, function(err, notifObj) { + if (err || !notifObj) { + return next(false); + } + + next(notifObj[field] === value.toString()); + }); + }, function(nids) { + callback(null, nids); }); - }, function(nids) { - callback(null, nids); }); }); }; + UserNotifications.sendPostNotificationToFollowers = function(uid, tid, pid) { db.getSetMembers('followers:' + uid, function(err, followers) { if (err || !followers || !followers.length) { @@ -184,7 +262,10 @@ var async = require('async'), path: nconf.get('relative_path') + '/topic/' + results.topic.slug + '/' + results.postIndex, uniqueId: 'topic:' + tid, from: uid - }, function(nid) { + }, function(err, nid) { + if (err) { + return; + } async.filter(followers, function(uid, next) { privileges.categories.can('read', results.topic.cid, uid, function(err, canRead) { next(!err && canRead);