From bf2133ed7c2d9e56eb1d8db110ec0f885ea60757 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 18 May 2016 19:02:43 +0300 Subject: [PATCH 1/6] topic watching --- public/language/en_GB/topic.json | 9 +- public/less/generics.less | 6 ++ public/src/client/topic/threadTools.js | 42 ++++++--- src/categories.js | 24 +++++ src/categories/delete.js | 1 + src/socket.io/helpers.js | 7 ++ src/socket.io/topics.js | 11 ++- src/topics.js | 9 +- src/topics/delete.js | 1 + src/topics/follow.js | 115 ++++++++++++++++++++++-- src/topics/unread.js | 6 ++ src/user.js | 13 +-- src/user/categories.js | 6 ++ src/user/delete.js | 8 +- src/user/posts.js | 15 +--- src/user/topics.js | 19 ++++ tests/topics.js | 119 +++++++++++++++++++++++-- 17 files changed, 362 insertions(+), 49 deletions(-) create mode 100644 src/user/topics.js diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index 6299b8aac1..a64bd15ca7 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -38,6 +38,7 @@ "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", "not_following_topic.message": "You will no longer receive notifications from this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", @@ -50,6 +51,12 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share_this_post": "Share this Post", + "following": "Following", + "reading": "Reading", + "ignoring": "Ignoring", + "following.description": "Notify me of new replies", + "reading.description": "Show topic in unread list", + "ignoring.description": "Do not show topic in unread list", "thread_tools.title": "Topic Tools", "thread_tools.markAsUnreadForAll": "Mark Unread", @@ -125,7 +132,7 @@ "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", "stale.reply_anyway": "Reply to this topic anyway", - + "link_back": "Re: [%1](%2)\n\n", "spam": "Spam", diff --git a/public/less/generics.less b/public/less/generics.less index 265007af55..33af3bfa11 100644 --- a/public/less/generics.less +++ b/public/less/generics.less @@ -21,6 +21,12 @@ overflow-x: hidden; } +.topic-watch-dropdown { + .help-text { + margin-left: 20px; + } +} + .category-list { padding: 0; diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index 433c12e575..adc110e8a6 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -83,12 +83,18 @@ define('forum/topic/threadTools', [ deletePosts.init(); fork.init(); - components.get('topic').on('click', '[component="topic/follow"], [component="topic/unfollow"]', follow); - components.get('topic/follow').off('click').on('click', follow); - components.get('topic/unfollow').off('click').on('click', follow); + $('.topic').on('click', '[component="topic/following"]', function() { + setFollow('follow'); + }); + $('.topic').on('click', '[component="topic/reading"]', function() { + setFollow('unfollow'); + }); + $('.topic').on('click', '[component="topic/ignoring"]', function() { + setFollow('ignore'); + }); - function follow() { - socket.emit('topics.toggleFollow', tid, function(err, state) { + function setFollow(type) { + socket.emit('topics.changeWatching', {tid: tid, type: type}, function(err) { if (err) { return app.alert({ type: 'danger', @@ -98,12 +104,19 @@ define('forum/topic/threadTools', [ timeout: 5000 }); } - - setFollowState(state); + var message = ''; + if (type === 'follow') { + message = '[[topic:following_topic.message]]'; + } else if (type === 'unfollow') { + message = '[[topic:not_following_topic.message]]'; + } else if (type === 'ignore') { + message = '[[topic:ignoring_topic.message]]'; + } + setFollowState(type); app.alert({ alert_id: 'follow_thread', - message: state ? '[[topic:following_topic.message]]' : '[[topic:not_following_topic.message]]', + message: message, type: 'success', timeout: 5000 }); @@ -195,8 +208,17 @@ define('forum/topic/threadTools', [ }; function setFollowState(state) { - components.get('topic/follow').toggleClass('hidden', state); - components.get('topic/unfollow').toggleClass('hidden', !state); + var menu = components.get('topic/following/menu'); + menu.toggleClass('hidden', state !== 'follow'); + components.get('topic/following/check').toggleClass('fa-check', state === 'follow'); + + menu = components.get('topic/reading/menu'); + menu.toggleClass('hidden', state !== 'unfollow'); + components.get('topic/reading/check').toggleClass('fa-check', state === 'unfollow'); + + menu = components.get('topic/ignoring/menu'); + menu.toggleClass('hidden', state !== 'ignore' ); + components.get('topic/ignoring/check').toggleClass('fa-check', state === 'ignore'); } diff --git a/src/categories.js b/src/categories.js index cd2b18a787..a000eff251 100644 --- a/src/categories.js +++ b/src/categories.js @@ -305,4 +305,28 @@ var privileges = require('./privileges'); return tree; }; + Categories.getIgnorers = function(cid, start, stop, callback) { + db.getSortedSetRevRange('cid:' + cid + ':ignorers', start, stop, callback); + }; + + Categories.filterIgnoringUids = function(cid, uids, callback) { + async.waterfall([ + function (next){ + Categories.getIgnorers(cid, 0, -1, next); + }, + function (ignorerUids, next){ + if (!ignorerUids.length) { + return next(null, uids); + } + + var readingUids = uids.filter(function(uid) { + return ignorerUids.indexOf(uid.toString()) === -1; + }); + + next(null, readingUids); + } + ], callback); + }; + + }(exports)); diff --git a/src/categories/delete.js b/src/categories/delete.js index 8d99bb60d1..c8a383037e 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -38,6 +38,7 @@ module.exports = function(Categories) { 'cid:' + cid + ':tids:posts', 'cid:' + cid + ':pids', 'cid:' + cid + ':read_by_uid', + 'cid:' + cid + ':ignorers', 'cid:' + cid + ':children', 'category:' + cid ], next); diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 69a4a26b54..148420dfb9 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -8,6 +8,7 @@ var websockets = require('./index'); var user = require('../user'); var posts = require('../posts'); var topics = require('../topics'); +var categories = require('../categories'); var privileges = require('../privileges'); var notifications = require('../notifications'); var plugins = require('../plugins'); @@ -27,6 +28,12 @@ SocketHelpers.notifyNew = function(uid, type, result) { function(uids, next) { privileges.topics.filterUids('read', result.posts[0].topic.tid, uids, next); }, + function(uids, next) { + topics.filterIgnoringUids(result.posts[0].topic.tid, uids, next); + }, + function(uids, next) { + categories.filterIgnoringUids(result.posts[0].topic.cid, uids, next); + }, function(uids, next) { plugins.fireHook('filter:sockets.sendNewPostToUids', {uidsTo: uids, uidFrom: uid, type: type}, next); } diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index 18cad2d980..451a1d0dce 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -72,8 +72,15 @@ SocketTopics.createTopicFromPosts = function(socket, data, callback) { topics.createTopicFromPosts(socket.uid, data.title, data.pids, data.fromTid, callback); }; -SocketTopics.toggleFollow = function(socket, tid, callback) { - followCommand(topics.toggleFollow, socket, tid, callback); +SocketTopics.changeWatching = function(socket, data, callback) { + if (!data.tid || !data.type) { + return callback(new Error('[[error:invalid-data]]')); + } + var commands = ['follow', 'unfollow', 'ignore']; + if (commands.indexOf(data.type) === -1) { + return callback(new Error('[[error:invalid-command]]')); + } + followCommand(topics[data.type], socket, data.tid, callback); }; SocketTopics.follow = function(socket, tid, callback) { diff --git a/src/topics.js b/src/topics.js index fbdd507d9a..8c09238e18 100644 --- a/src/topics.js +++ b/src/topics.js @@ -131,6 +131,9 @@ var social = require('./social'); hasRead: function(next) { Topics.hasReadTopics(tids, uid, next); }, + isIgnored: function(next) { + Topics.isIgnoring(tids, uid, next); + }, bookmarks: function(next) { Topics.getUserBookmarks(tids, uid, next); }, @@ -157,7 +160,8 @@ var social = require('./social'); topics[i].pinned = parseInt(topics[i].pinned, 10) === 1; topics[i].locked = parseInt(topics[i].locked, 10) === 1; topics[i].deleted = parseInt(topics[i].deleted, 10) === 1; - topics[i].unread = !results.hasRead[i]; + topics[i].ignored = results.isIgnored[i]; + topics[i].unread = !results.hasRead[i] && !results.isIgnored[i]; topics[i].bookmark = results.bookmarks[i]; topics[i].unreplied = !topics[i].teaser; } @@ -184,6 +188,7 @@ var social = require('./social'); threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', {topic: topicData, uid: uid, tools: []}), tags: async.apply(Topics.getTopicTagsObjects, topicData.tid), isFollowing: async.apply(Topics.isFollowing, [topicData.tid], uid), + isIgnoring: async.apply(Topics.isIgnoring, [topicData.tid], uid), bookmark: async.apply(Topics.getUserBookmark, topicData.tid, uid), postSharing: async.apply(social.getActivePostSharing) }, next); @@ -194,6 +199,8 @@ var social = require('./social'); topicData.thread_tools = results.threadTools.tools; topicData.tags = results.tags; topicData.isFollowing = results.isFollowing[0]; + topicData.isReading = !results.isFollowing[0] && !results.isIgnoring[0]; + topicData.isIgnoring = results.isIgnoring[0]; topicData.bookmark = results.bookmark; topicData.postSharing = results.postSharing; diff --git a/src/topics/delete.js b/src/topics/delete.js index f979d3083a..a960ff0421 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -113,6 +113,7 @@ module.exports = function(Topics) { function(next) { db.deleteAll([ 'tid:' + tid + ':followers', + 'tid:' + tid + ':ignorers', 'tid:' + tid + ':posts', 'tid:' + tid + ':posts:votes', 'tid:' + tid + ':bookmarks', diff --git a/src/topics/follow.js b/src/topics/follow.js index 209151d8e5..7b4054ac03 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -56,12 +56,12 @@ module.exports = function(Topics) { if (!exists) { return next(new Error('[[error:no-topic]]')); } - db.setAdd('tid:' + tid + ':followers', uid, next); + follow(tid, uid, next); }, - async.apply(plugins.fireHook, 'action:topic.follow', { uid: uid, tid: tid }), - function(next) { - db.sortedSetAdd('uid:' + uid + ':followed_tids', Date.now(), tid, next); - } + function (next) { + unignore(tid, uid, next); + }, + async.apply(plugins.fireHook, 'action:topic.follow', {uid: uid, tid: tid}) ], callback); }; @@ -75,14 +75,77 @@ module.exports = function(Topics) { if (!exists) { return next(new Error('[[error:no-topic]]')); } + unfollow(tid, uid, next); + }, + function (next) { + unignore(tid, uid, next); + }, + async.apply(plugins.fireHook, 'action:topic.unfollow', {uid: uid, tid: tid}), + ], callback); + }; + + Topics.ignore = function(tid, uid, callback) { + callback = callback || function() {}; + async.waterfall([ + function (next) { + Topics.exists(tid, next); + }, + function (exists, next) { + if (!exists) { + return next(new Error('[[error:no-topic]]')); + } + ignore(tid, uid, next); + }, + function (next) { + unfollow(tid, uid, next); + }, + async.apply(plugins.fireHook, 'action:topic.ignore', {uid: uid, tid: tid}) + ], callback); + }; + + function follow(tid, uid, callback) { + async.waterfall([ + function (next) { + db.setAdd('tid:' + tid + ':followers', uid, next); + }, + function (next) { + db.sortedSetAdd('uid:' + uid + ':followed_tids', Date.now(), tid, next); + } + ], callback); + } + + function unfollow(tid, uid, callback) { + async.waterfall([ + function (next) { db.setRemove('tid:' + tid + ':followers', uid, next); }, - async.apply(plugins.fireHook, 'action:topic.unfollow', { uid: uid, tid: tid }), - function(next) { + function (next) { db.sortedSetRemove('uid:' + uid + ':followed_tids', tid, next); } ], callback); - }; + } + + function ignore(tid, uid, callback) { + async.waterfall([ + function (next) { + db.setAdd('tid:' + tid + ':ignorers', uid, next); + }, + function(next) { + db.sortedSetAdd('uid:' + uid + ':ignored_tids', Date.now(), tid, next); + } + ], callback); + } + + function unignore(tid, uid, callback) { + async.waterfall([ + function (next) { + db.setRemove('tid:' + tid + ':ignorers', uid, next); + }, + function(next) { + db.sortedSetRemove('uid:' + uid + ':ignored_tids', tid, next); + } + ], callback); + } Topics.isFollowing = function(tids, uid, callback) { if (!Array.isArray(tids)) { @@ -97,10 +160,46 @@ module.exports = function(Topics) { db.isMemberOfSets(keys, uid, callback); }; + Topics.isIgnoring = function(tids, uid, callback) { + if (!Array.isArray(tids)) { + return callback(); + } + if (!parseInt(uid, 10)) { + return callback(null, tids.map(function() { return false; })); + } + var keys = tids.map(function(tid) { + return 'tid:' + tid + ':ignorers'; + }); + db.isMemberOfSets(keys, uid, callback); + }; + Topics.getFollowers = function(tid, callback) { db.getSetMembers('tid:' + tid + ':followers', callback); }; + Topics.getIgnorers = function(tid, callback) { + db.getSetMembers('tid:' + tid + ':ignorers', callback); + }; + + Topics.filterIgnoringUids = function(tid, uids, callback){ + async.waterfall([ + function (next){ + Topics.getIgnorers(tid, next); + }, + function (ignorerUids, next){ + if (!ignorerUids.length) { + return next(null, uids); + } + + var readingUids = uids.filter(function(uid) { + return ignorerUids.indexOf(uid.toString()) === -1; + }); + + next(null, readingUids); + } + ], callback); + }; + Topics.notifyFollowers = function(postData, exceptUid, callback) { callback = callback || function() {}; var followers; diff --git a/src/topics/unread.js b/src/topics/unread.js index 9dcfd8bdd9..c62ae5ac3a 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -87,6 +87,9 @@ module.exports = function(Topics) { } user.getIgnoredCategories(uid, next); }, + ignoredTids: function(next) { + user.getIgnoredTids(uid, 0, -1, next); + }, recentTids: function(next) { db.getSortedSetRevRangeByScoreWithScores('topics:recent', 0, -1, '+inf', cutoff, next); }, @@ -116,6 +119,9 @@ module.exports = function(Topics) { }); var tids = results.recentTids.filter(function(recentTopic) { + if (results.ignoredTids.indexOf(recentTopic.value.toString()) !== -1) { + return false; + } switch (filter) { case 'new': return !userRead[recentTopic.value]; diff --git a/src/user.js b/src/user.js index 19c60fc1fd..c7fb18e4fc 100644 --- a/src/user.js +++ b/src/user.js @@ -1,12 +1,12 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - plugins = require('./plugins'), - db = require('./database'), - topics = require('./topics'), - privileges = require('./privileges'), - utils = require('../public/src/utils'); +var plugins = require('./plugins'); +var db = require('./database'); +var topics = require('./topics'); +var privileges = require('./privileges'); +var utils = require('../public/src/utils'); (function(User) { @@ -19,6 +19,7 @@ var async = require('async'), require('./user/auth')(User); require('./user/create')(User); require('./user/posts')(User); + require('./user/topics')(User); require('./user/categories')(User); require('./user/follow')(User); require('./user/profile')(User); diff --git a/src/user/categories.js b/src/user/categories.js index 612b5ec14e..7a87f5e441 100644 --- a/src/user/categories.js +++ b/src/user/categories.js @@ -45,6 +45,9 @@ module.exports = function(User) { return next(new Error('[[error:no-category]]')); } db.sortedSetAdd('uid:' + uid + ':ignored:cids', Date.now(), cid, next); + }, + function (next) { + db.sortedSetAdd('cid:' + cid + ':ignorers', Date.now(), uid, next); } ], callback); }; @@ -63,6 +66,9 @@ module.exports = function(User) { return next(new Error('[[error:no-category]]')); } db.sortedSetRemove('uid:' + uid + ':ignored:cids', cid, next); + }, + function (next) { + db.sortedSetRemove('cid:' + cid + ':ignorers', uid, next); } ], callback); }; diff --git a/src/user/delete.js b/src/user/delete.js index 99119c7ffb..6c5a92e225 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -102,8 +102,12 @@ module.exports = function(User) { }, function(next) { var keys = [ - 'uid:' + uid + ':notifications:read', 'uid:' + uid + ':notifications:unread', - 'uid:' + uid + ':favourites', 'uid:' + uid + ':followed_tids', 'user:' + uid + ':settings', + 'uid:' + uid + ':notifications:read', + 'uid:' + uid + ':notifications:unread', + 'uid:' + uid + ':favourites', + 'uid:' + uid + ':followed_tids', + 'uid:' + uid + ':ignored_tids', + 'user:' + uid + ':settings', 'uid:' + uid + ':topics', 'uid:' + uid + ':posts', 'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread', 'uid:' + uid + ':chat:rooms', 'uid:' + uid + ':chat:rooms:unread', diff --git a/src/user/posts.js b/src/user/posts.js index 78a2db0923..c5d8cfba1a 100644 --- a/src/user/posts.js +++ b/src/user/posts.js @@ -1,9 +1,9 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - meta = require('../meta'), - privileges = require('../privileges'); +var async = require('async'); +var db = require('../database'); +var meta = require('../meta'); +var privileges = require('../privileges'); module.exports = function(User) { @@ -83,13 +83,6 @@ module.exports = function(User) { db.sortedSetAdd('uid:' + uid + ':posts', timestamp, pid, callback); }; - User.addTopicIdToUser = function(uid, tid, timestamp, callback) { - async.parallel([ - async.apply(db.sortedSetAdd, 'uid:' + uid + ':topics', timestamp, tid), - async.apply(User.incrementUserFieldBy, uid, 'topiccount', 1) - ], callback); - }; - User.incrementUserPostCountBy = function(uid, value, callback) { callback = callback || function() {}; User.incrementUserFieldBy(uid, 'postcount', value, function(err, newpostcount) { diff --git a/src/user/topics.js b/src/user/topics.js new file mode 100644 index 0000000000..2df535a1ce --- /dev/null +++ b/src/user/topics.js @@ -0,0 +1,19 @@ +'use strict'; + +var async = require('async'); +var db = require('../database'); + +module.exports = function(User) { + + User.getIgnoredTids = function(uid, start, stop, callback) { + db.getSortedSetRevRange('uid:' + uid + ':ignored_tids', start, stop, callback); + }; + + User.addTopicIdToUser = function(uid, tid, timestamp, callback) { + async.parallel([ + async.apply(db.sortedSetAdd, 'uid:' + uid + ':topics', timestamp, tid), + async.apply(User.incrementUserFieldBy, uid, 'topiccount', 1) + ], callback); + }; + +}; \ No newline at end of file diff --git a/tests/topics.js b/tests/topics.js index 60c71960cd..ebf97f17b6 100644 --- a/tests/topics.js +++ b/tests/topics.js @@ -57,28 +57,28 @@ describe('Topic\'s', function() { }); it('should fail to create new topic with invalid user id', function(done) { - topics.post({uid: null, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { + topics.post({uid: null, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err) { assert.equal(err.message, '[[error:no-privileges]]'); done(); }); }); it('should fail to create new topic with empty title', function(done) { - topics.post({uid: topic.userId, title: '', content: topic.content, cid: topic.categoryId}, function(err, result) { + topics.post({uid: topic.userId, title: '', content: topic.content, cid: topic.categoryId}, function(err) { assert.ok(err); done(); }); }); it('should fail to create new topic with empty content', function(done) { - topics.post({uid: topic.userId, title: topic.title, content: '', cid: topic.categoryId}, function(err, result) { + topics.post({uid: topic.userId, title: topic.title, content: '', cid: topic.categoryId}, function(err) { assert.ok(err); done(); }); }); it('should fail to create new topic with non-existant category id', function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: 99}, function(err, result) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: 99}, function(err) { assert.equal(err.message, '[[error:no-category]]', 'received no error'); done(); }); @@ -107,21 +107,21 @@ describe('Topic\'s', function() { }); it('should fail to create new reply with invalid user id', function(done) { - topics.reply({uid: null, content: 'test post', tid: newTopic.tid}, function(err, result) { + topics.reply({uid: null, content: 'test post', tid: newTopic.tid}, function(err) { assert.equal(err.message, '[[error:no-privileges]]'); done(); }); }); it('should fail to create new reply with empty content', function(done) { - topics.reply({uid: topic.userId, content: '', tid: newTopic.tid}, function(err, result) { + topics.reply({uid: topic.userId, content: '', tid: newTopic.tid}, function(err) { assert.ok(err); done(); }); }); it('should fail to create new reply with invalid topic id', function(done) { - topics.reply({uid: null, content: 'test post', tid: 99}, function(err, result) { + topics.reply({uid: null, content: 'test post', tid: 99}, function(err) { assert.equal(err.message, '[[error:no-topic]]'); done(); }); @@ -189,10 +189,113 @@ describe('Topic\'s', function() { }); }); + describe('.ignore', function(){ + var newTid; + var uid; + var newTopic; + before(function(done){ + uid = topic.userId; + async.waterfall([ + function(done){ + topics.post({uid: topic.userId, title: 'Topic to be ignored', content: 'Just ignore me, please!', cid: topic.categoryId}, function(err, result) { + newTopic = result.topicData; + newTid = newTopic.tid; + done(); + }); + }, + function(done){ + topics.markUnread( newTid, uid, done ); + } + ],done); + }); + + it('should not appear in the unread list', function(done){ + async.waterfall([ + function(done){ + topics.ignore( newTid, uid, done ); + }, + function(done){ + topics.getUnreadTopics(0, uid, 0, -1, done ); + }, + function(results, done){ + var topics = results.topics; + var tids = topics.map( function(topic){ return topic.tid; } ); + assert.equal(tids.indexOf(newTid), -1, 'The topic appeared in the unread list.'); + done(); + } + ], done); + }); + + it('should not appear as unread in the recent list', function(done){ + async.waterfall([ + function(done){ + topics.ignore( newTid, uid, done ); + }, + function(done){ + topics.getLatestTopics( uid, 0, -1, 'year', done ); + }, + function(results, done){ + var topics = results.topics; + var topic; + var i; + for(i = 0; i < topics.length; ++i){ + if( topics[i].tid == newTid ){ + assert.equal(false, topics[i].unread, 'ignored topic was marked as unread in recent list'); + return done(); + } + } + assert.ok(topic, 'topic didn\'t appear in the recent list'); + done(); + } + ], done); + }); + + it('should appear as unread again when marked as reading', function(done){ + async.waterfall([ + function(done){ + topics.ignore( newTid, uid, done ); + }, + function(done){ + topics.follow( newTid, uid, done ); + }, + function(done){ + topics.getUnreadTopics(0, uid, 0, -1, done ); + }, + function(results, done){ + var topics = results.topics; + var tids = topics.map( function(topic){ return topic.tid; } ); + assert.notEqual(tids.indexOf(newTid), -1, 'The topic did not appear in the unread list.'); + done(); + } + ], done); + }); + + it('should appear as unread again when marked as following', function(done){ + async.waterfall([ + function(done){ + topics.ignore( newTid, uid, done ); + }, + function(done){ + topics.follow( newTid, uid, done ); + }, + function(done){ + topics.getUnreadTopics(0, uid, 0, -1, done ); + }, + function(results, done){ + var topics = results.topics; + var tids = topics.map( function(topic){ return topic.tid; } ); + assert.notEqual(tids.indexOf(newTid), -1, 'The topic did not appear in the unread list.'); + done(); + } + ], done); + }); + }); + + describe('.fork', function(){ var newTopic; - var replies = new Array(); + var replies = []; var topicPids; var originalBookmark = 5; function postReply( next ){ From 5d2e5377c5e570f3987980f83288666dfe7108ce Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 18 May 2016 19:22:26 +0300 Subject: [PATCH 2/6] fix tests --- tests/topics.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/topics.js b/tests/topics.js index ebf97f17b6..0c04d875bf 100644 --- a/tests/topics.js +++ b/tests/topics.js @@ -215,7 +215,7 @@ describe('Topic\'s', function() { topics.ignore( newTid, uid, done ); }, function(done){ - topics.getUnreadTopics(0, uid, 0, -1, done ); + topics.getUnreadTopics(0, uid, 0, -1, '', done ); }, function(results, done){ var topics = results.topics; @@ -259,7 +259,7 @@ describe('Topic\'s', function() { topics.follow( newTid, uid, done ); }, function(done){ - topics.getUnreadTopics(0, uid, 0, -1, done ); + topics.getUnreadTopics(0, uid, 0, -1, '', done ); }, function(results, done){ var topics = results.topics; @@ -279,7 +279,7 @@ describe('Topic\'s', function() { topics.follow( newTid, uid, done ); }, function(done){ - topics.getUnreadTopics(0, uid, 0, -1, done ); + topics.getUnreadTopics(0, uid, 0, -1, '', done ); }, function(results, done){ var topics = results.topics; From a141f6f7ec2a736addc2e61151b125259880fc51 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 18 May 2016 20:02:34 +0300 Subject: [PATCH 3/6] updated category watch control --- public/language/en_GB/category.json | 4 ++++ public/src/client/category.js | 11 +++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/public/language/en_GB/category.json b/public/language/en_GB/category.json index f8b02f32f8..d4da76f356 100644 --- a/public/language/en_GB/category.json +++ b/public/language/en_GB/category.json @@ -13,6 +13,10 @@ "share_this_category": "Share this category", "watch": "Watch", "ignore": "Ignore", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", "watch.message": "You are now watching updates from this category", "ignore.message": "You are now ignoring updates from this category", diff --git a/public/src/client/category.js b/public/src/client/category.js index a441ff5d08..5f44197a6c 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -62,17 +62,20 @@ define('forum/category', [ }; function handleIgnoreWatch(cid) { - $('.watch, .ignore').on('click', function() { + $('[component="category/watching"], [component="category/ignoring"]').on('click', function() { var $this = $(this); - var command = $this.hasClass('watch') ? 'watch' : 'ignore'; + var command = $this.attr('component') === 'category/watching' ? 'watch' : 'ignore'; socket.emit('categories.' + command, cid, function(err) { if (err) { return app.alertError(err.message); } - $('.watch').toggleClass('hidden', command === 'watch'); - $('.ignore').toggleClass('hidden', command === 'ignore'); + $('[component="category/watching/menu"]').toggleClass('hidden', command !== 'watch'); + $('[component="category/watching/check"]').toggleClass('fa-check', command === 'watch'); + + $('[component="category/ignoring/menu"]').toggleClass('hidden', command !== 'ignore'); + $('[component="category/ignoring/check"]').toggleClass('fa-check', command === 'ignore'); app.alertSuccess('[[category:' + command + '.message]]'); }); From 7a044b4978f6a0836762617e23d9ddf37d95fa51 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 19 May 2016 11:06:17 +0300 Subject: [PATCH 4/6] better filterIgnoringUids --- src/categories.js | 13 ++++--------- src/topics/follow.js | 15 +++++---------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/categories.js b/src/categories.js index a000eff251..539a643f80 100644 --- a/src/categories.js +++ b/src/categories.js @@ -312,17 +312,12 @@ var privileges = require('./privileges'); Categories.filterIgnoringUids = function(cid, uids, callback) { async.waterfall([ function (next){ - Categories.getIgnorers(cid, 0, -1, next); + db.sortedSetScores('cid:' + cid + ':ignorers', uids, next); }, - function (ignorerUids, next){ - if (!ignorerUids.length) { - return next(null, uids); - } - - var readingUids = uids.filter(function(uid) { - return ignorerUids.indexOf(uid.toString()) === -1; + function (scores, next) { + var readingUids = uids.filter(function(uid, index) { + return uid && !!scores[index]; }); - next(null, readingUids); } ], callback); diff --git a/src/topics/follow.js b/src/topics/follow.js index 7b4054ac03..42143b5641 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -181,20 +181,15 @@ module.exports = function(Topics) { db.getSetMembers('tid:' + tid + ':ignorers', callback); }; - Topics.filterIgnoringUids = function(tid, uids, callback){ + Topics.filterIgnoringUids = function(tid, uids, callback) { async.waterfall([ function (next){ - Topics.getIgnorers(tid, next); + db.isSetMembers('tid:' + tid + ':ignorers', uids, next); }, - function (ignorerUids, next){ - if (!ignorerUids.length) { - return next(null, uids); - } - - var readingUids = uids.filter(function(uid) { - return ignorerUids.indexOf(uid.toString()) === -1; + function (isMembers, next){ + var readingUids = uids.filter(function(uid, index) { + return uid && isMembers[index]; }); - next(null, readingUids); } ], callback); From 11d3834eb90b680cc6525fe81903dd5e9341b70b Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 19 May 2016 13:11:42 +0300 Subject: [PATCH 5/6] better filtering if topic is followed but category ignored show it in unread --- public/language/en_GB/topic.json | 4 ++-- src/socket.io/helpers.js | 32 +++++++++++++++++++++++++++----- src/topics/unread.js | 25 +++++++++++++++++++------ 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index a64bd15ca7..176e9daa52 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -55,8 +55,8 @@ "reading": "Reading", "ignoring": "Ignoring", "following.description": "Notify me of new replies", - "reading.description": "Show topic in unread list", - "ignoring.description": "Do not show topic in unread list", + "reading.description": "Show topic in unread", + "ignoring.description": "Do not show topic in unread", "thread_tools.title": "Topic Tools", "thread_tools.markAsUnreadForAll": "Mark Unread", diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 148420dfb9..68182293f4 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -4,11 +4,11 @@ var async = require('async'); var winston = require('winston'); var S = require('string'); +var db = require('../database'); var websockets = require('./index'); var user = require('../user'); var posts = require('../posts'); var topics = require('../topics'); -var categories = require('../categories'); var privileges = require('../privileges'); var notifications = require('../notifications'); var plugins = require('../plugins'); @@ -29,10 +29,7 @@ SocketHelpers.notifyNew = function(uid, type, result) { privileges.topics.filterUids('read', result.posts[0].topic.tid, uids, next); }, function(uids, next) { - topics.filterIgnoringUids(result.posts[0].topic.tid, uids, next); - }, - function(uids, next) { - categories.filterIgnoringUids(result.posts[0].topic.cid, uids, next); + filterTidCidIgnorers(uids, result.posts[0].topic.tid, result.posts[0].topic.cid, next); }, function(uids, next) { plugins.fireHook('filter:sockets.sendNewPostToUids', {uidsTo: uids, uidFrom: uid, type: type}, next); @@ -55,6 +52,31 @@ SocketHelpers.notifyNew = function(uid, type, result) { }); }; +function filterTidCidIgnorers(uids, tid, cid, callback) { + async.waterfall([ + function (next) { + async.parallel({ + topicFollowed: function(next) { + db.isSetMembers('tid:' + tid + ':followers', uids, next); + }, + topicIgnored: function(next) { + db.isSetMembers('tid:' + tid + ':ignorers', uids, next); + }, + categoryIgnored: function(next) { + db.sortedSetScores('cid:' + cid + ':ignorers', uids, next); + } + }, next); + }, + function (results, next) { + uids = uids.filter(function(uid, index) { + return results.topicFollowed[index] || + (!results.topicFollowed[index] && !results.topicIgnored[index] && !results.categoryIgnored[index]); + }); + next(null, uids); + } + ], callback); +} + SocketHelpers.sendNotificationToPostOwner = function(pid, fromuid, notification) { if (!pid || !fromuid || !notification) { return; diff --git a/src/topics/unread.js b/src/topics/unread.js index c62ae5ac3a..b4900be3a1 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -144,7 +144,7 @@ module.exports = function(Topics) { tids = tids.slice(0, 200); - filterTopics(uid, tids, cid, ignoredCids, next); + filterTopics(uid, tids, cid, ignoredCids, filter, next); } ], callback); }; @@ -161,7 +161,7 @@ module.exports = function(Topics) { }); } - function filterTopics(uid, tids, cid, ignoredCids, callback) { + function filterTopics(uid, tids, cid, ignoredCids, filter, callback) { if (!Array.isArray(ignoredCids) || !tids.length) { return callback(null, tids); } @@ -171,11 +171,24 @@ module.exports = function(Topics) { privileges.topics.filterTids('read', tids, uid, next); }, function(tids, next) { - Topics.getTopicsFields(tids, ['tid', 'cid'], next); + async.parallel({ + topics: function(next) { + Topics.getTopicsFields(tids, ['tid', 'cid'], next); + }, + isTopicsFollowed: function(next) { + if (filter === 'watched' || filter === 'new') { + return next(null, []); + } + db.sortedSetScores('uid:' + uid + ':followed_tids', tids, next); + } + }, next); }, - function(topics, next) { - tids = topics.filter(function(topic) { - return topic && topic.cid && ignoredCids.indexOf(topic.cid.toString()) === -1 && (!cid || parseInt(cid, 10) === parseInt(topic.cid, 10)); + function(results, next) { + var topics = results.topics; + tids = topics.filter(function(topic, index) { + return topic && topic.cid && + (!!results.isTopicsFollowed[index] || ignoredCids.indexOf(topic.cid.toString()) === -1) && + (!cid || parseInt(cid, 10) === parseInt(topic.cid, 10)); }).map(function(topic) { return topic.tid; }); From b02869b0f8d58e3ce81e46dac4dffefe2ded780c Mon Sep 17 00:00:00 2001 From: barisusakli Date: Thu, 19 May 2016 14:20:34 +0300 Subject: [PATCH 6/6] renamed reading to not-following added more description text --- public/language/en_GB/topic.json | 10 +++++----- public/src/client/topic/threadTools.js | 14 +++++++------- src/topics.js | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index 176e9daa52..11b835981b 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -51,12 +51,12 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share_this_post": "Share this Post", - "following": "Following", - "reading": "Reading", + "watching": "Watching", + "not-watching": "Not Watching", "ignoring": "Ignoring", - "following.description": "Notify me of new replies", - "reading.description": "Show topic in unread", - "ignoring.description": "Do not show topic in unread", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", "thread_tools.title": "Topic Tools", "thread_tools.markAsUnreadForAll": "Mark Unread", diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index adc110e8a6..605981f484 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -84,16 +84,16 @@ define('forum/topic/threadTools', [ fork.init(); $('.topic').on('click', '[component="topic/following"]', function() { - setFollow('follow'); + changeWatching('follow'); }); - $('.topic').on('click', '[component="topic/reading"]', function() { - setFollow('unfollow'); + $('.topic').on('click', '[component="topic/not-following"]', function() { + changeWatching('unfollow'); }); $('.topic').on('click', '[component="topic/ignoring"]', function() { - setFollow('ignore'); + changeWatching('ignore'); }); - function setFollow(type) { + function changeWatching(type) { socket.emit('topics.changeWatching', {tid: tid, type: type}, function(err) { if (err) { return app.alert({ @@ -212,9 +212,9 @@ define('forum/topic/threadTools', [ menu.toggleClass('hidden', state !== 'follow'); components.get('topic/following/check').toggleClass('fa-check', state === 'follow'); - menu = components.get('topic/reading/menu'); + menu = components.get('topic/not-following/menu'); menu.toggleClass('hidden', state !== 'unfollow'); - components.get('topic/reading/check').toggleClass('fa-check', state === 'unfollow'); + components.get('topic/not-following/check').toggleClass('fa-check', state === 'unfollow'); menu = components.get('topic/ignoring/menu'); menu.toggleClass('hidden', state !== 'ignore' ); diff --git a/src/topics.js b/src/topics.js index 8c09238e18..45f87ebfec 100644 --- a/src/topics.js +++ b/src/topics.js @@ -199,7 +199,7 @@ var social = require('./social'); topicData.thread_tools = results.threadTools.tools; topicData.tags = results.tags; topicData.isFollowing = results.isFollowing[0]; - topicData.isReading = !results.isFollowing[0] && !results.isIgnoring[0]; + topicData.isNotFollowing = !results.isFollowing[0] && !results.isIgnoring[0]; topicData.isIgnoring = results.isIgnoring[0]; topicData.bookmark = results.bookmark; topicData.postSharing = results.postSharing;