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/language/en_GB/topic.json b/public/language/en_GB/topic.json
index 6299b8aac1..11b835981b 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",
+ "watching": "Watching",
+ "not-watching": "Not Watching",
+ "ignoring": "Ignoring",
+ "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",
@@ -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/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]]');
});
diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js
index 433c12e575..605981f484 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() {
+ changeWatching('follow');
+ });
+ $('.topic').on('click', '[component="topic/not-following"]', function() {
+ changeWatching('unfollow');
+ });
+ $('.topic').on('click', '[component="topic/ignoring"]', function() {
+ changeWatching('ignore');
+ });
- function follow() {
- socket.emit('topics.toggleFollow', tid, function(err, state) {
+ function changeWatching(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/not-following/menu');
+ menu.toggleClass('hidden', state !== 'unfollow');
+ components.get('topic/not-following/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..539a643f80 100644
--- a/src/categories.js
+++ b/src/categories.js
@@ -305,4 +305,23 @@ 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){
+ db.sortedSetScores('cid:' + cid + ':ignorers', uids, next);
+ },
+ function (scores, next) {
+ var readingUids = uids.filter(function(uid, index) {
+ return uid && !!scores[index];
+ });
+ 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..68182293f4 100644
--- a/src/socket.io/helpers.js
+++ b/src/socket.io/helpers.js
@@ -4,6 +4,7 @@ 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');
@@ -27,6 +28,9 @@ SocketHelpers.notifyNew = function(uid, type, result) {
function(uids, next) {
privileges.topics.filterUids('read', result.posts[0].topic.tid, uids, next);
},
+ function(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);
}
@@ -48,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/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..45f87ebfec 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.isNotFollowing = !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..42143b5641 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,41 @@ 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){
+ db.isSetMembers('tid:' + tid + ':ignorers', uids, next);
+ },
+ function (isMembers, next){
+ var readingUids = uids.filter(function(uid, index) {
+ return uid && isMembers[index];
+ });
+ 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..b4900be3a1 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];
@@ -138,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);
};
@@ -155,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);
}
@@ -165,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;
});
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..0c04d875bf 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 ){