diff --git a/install/package.json b/install/package.json
index 0de3368deb..d83641ec68 100644
--- a/install/package.json
+++ b/install/package.json
@@ -78,7 +78,7 @@
         "nodebb-theme-persona": "9.0.15",
         "nodebb-theme-slick": "1.2.4",
         "nodebb-theme-vanilla": "10.0.13",
-        "nodebb-widget-essentials": "4.0.4",
+        "nodebb-widget-essentials": "4.0.6",
         "nodemailer": "^4.6.5",
         "passport": "^0.4.0",
         "passport-local": "1.0.0",
diff --git a/src/topics.js b/src/topics.js
index 8cea101b84..8aa00488a0 100644
--- a/src/topics.js
+++ b/src/topics.js
@@ -228,10 +228,6 @@ Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse,
 			topicData.locked = parseInt(topicData.locked, 10) === 1;
 			topicData.pinned = parseInt(topicData.pinned, 10) === 1;
 
-			topicData.upvotes = parseInt(topicData.upvotes, 10) || 0;
-			topicData.downvotes = parseInt(topicData.downvotes, 10) || 0;
-			topicData.votes = topicData.upvotes - topicData.downvotes;
-
 			topicData.icons = [];
 
 			plugins.fireHook('filter:topic.get', { topic: topicData, uid: uid }, next);
diff --git a/src/topics/data.js b/src/topics/data.js
index 6c5f970ff9..36a0d36206 100644
--- a/src/topics/data.js
+++ b/src/topics/data.js
@@ -98,6 +98,15 @@ module.exports = function (Topics) {
 		escapeTitle(topic);
 		topic.timestampISO = utils.toISOString(topic.timestamp);
 		topic.lastposttimeISO = utils.toISOString(topic.lastposttime);
+		if (topic.hasOwnProperty('upvotes')) {
+			topic.upvotes = parseInt(topic.upvotes, 10) || 0;
+		}
+		if (topic.hasOwnProperty('upvotes')) {
+			topic.downvotes = parseInt(topic.downvotes, 10) || 0;
+		}
+		if (topic.hasOwnProperty('upvotes') && topic.hasOwnProperty('downvotes')) {
+			topic.votes = topic.upvotes - topic.downvotes;
+		}
 	}
 
 	Topics.getCategoryData = function (tid, callback) {
diff --git a/src/topics/delete.js b/src/topics/delete.js
index 135a82be00..e44e332a1a 100644
--- a/src/topics/delete.js
+++ b/src/topics/delete.js
@@ -74,9 +74,7 @@ module.exports = function (Topics) {
 						db.sortedSetAdd('topics:views', topicData.viewcount, tid, next);
 					},
 					function (next) {
-						var upvotes = parseInt(topicData.upvotes, 10) || 0;
-						var downvotes = parseInt(topicData.downvotes, 10) || 0;
-						db.sortedSetAdd('topics:votes', upvotes - downvotes, tid, next);
+						db.sortedSetAdd('topics:votes', topicData.votes, tid, next);
 					},
 					function (next) {
 						async.waterfall([
diff --git a/src/topics/popular.js b/src/topics/popular.js
index 95bed19613..c1ff1ddf8a 100644
--- a/src/topics/popular.js
+++ b/src/topics/popular.js
@@ -1,73 +1,14 @@
 
 'use strict';
 
-var async = require('async');
-
-var db = require('../database');
-var privileges = require('../privileges');
-
 module.exports = function (Topics) {
-	Topics.getPopular = function (term, uid, count, callback) {
-		count = parseInt(count, 10) || 20;
-		async.waterfall([
-			function (next) {
-				Topics.getPopularTopics(term, uid, 0, count - 1, next);
-			},
-			function (data, next) {
-				next(null, data.topics);
-			},
-		], callback);
-	};
-
 	Topics.getPopularTopics = function (term, uid, start, stop, callback) {
-		var popularTopics = {
-			nextStart: 0,
-			topicCount: 0,
-			topics: [],
-		};
-		async.waterfall([
-			function (next) {
-				if (term === 'alltime') {
-					db.getSortedSetRevRange('topics:posts', 0, 199, next);
-				} else {
-					Topics.getLatestTidsFromSet('topics:tid', 0, -1, term, next);
-				}
-			},
-			function (tids, next) {
-				popularTopics.topicCount = tids.length;
-				getTopics(tids, uid, start, stop, next);
-			},
-			function (topics, next) {
-				popularTopics.topics = topics;
-				popularTopics.nextStart = stop + 1;
-				next(null, popularTopics);
-			},
-		], callback);
+		Topics.getSortedTopics({
+			uid: uid,
+			start: start,
+			stop: stop,
+			term: term,
+			sort: 'posts',
+		}, callback);
 	};
-
-	function getTopics(tids, uid, start, stop, callback) {
-		async.waterfall([
-			function (next) {
-				Topics.getTopicsFields(tids, ['tid', 'postcount', 'deleted'], next);
-			},
-			function (topics, next) {
-				tids = topics.filter(function (topic) {
-					return topic && parseInt(topic.deleted, 10) !== 1;
-				}).sort(sortPopular).slice(start, stop !== -1 ? stop + 1 : undefined).map(function (topic) {
-					return topic.tid;
-				});
-				privileges.topics.filterTids('read', tids, uid, next);
-			},
-			function (tids, next) {
-				Topics.getTopicsByTids(tids, uid, next);
-			},
-		], callback);
-	}
-
-	function sortPopular(a, b) {
-		if (parseInt(a.postcount, 10) !== parseInt(b.postcount, 10)) {
-			return b.postcount - a.postcount;
-		}
-		return parseInt(b.viewcount, 10) - parseInt(a.viewcount, 10);
-	}
 };
diff --git a/src/topics/recent.js b/src/topics/recent.js
index 6fea81f972..1556e081bc 100644
--- a/src/topics/recent.js
+++ b/src/topics/recent.js
@@ -18,44 +18,135 @@ module.exports = function (Topics) {
 		year: 31104000000,
 	};
 
-	Topics.getRecentTopics = function (cid, uid, start, stop, filter, callback) {
-		var recentTopics = {
+	Topics.getSortedTopics = function (params, callback) {
+		var data = {
 			nextStart: 0,
+			topicCount: 0,
 			topics: [],
 		};
-		if (cid && !Array.isArray(cid)) {
-			cid = [cid];
+
+		params.term = params.term || 'alltime';
+		if (params.hasOwnProperty('cids') && params.cids && !Array.isArray(params.cids)) {
+			params.cids = [params.cids];
 		}
+
 		async.waterfall([
 			function (next) {
-				var key = 'topics:recent';
-				if (cid) {
-					key = cid.map(function (cid) {
-						return 'cid:' + cid + ':tids:lastposttime';
-					});
+				getTids(params, next);
+			},
+			function (tids, next) {
+				data.topicCount = tids.length;
+				getTopics(tids, params, next);
+			},
+			function (topicData, next) {
+				data.topics = topicData;
+				data.nextStart = params.stop + 1;
+				next(null, data);
+			},
+		], callback);
+	};
+
+	function getTids(params, callback) {
+		async.waterfall([
+			function (next) {
+				if (params.term === 'alltime') {
+					var key = 'topics:' + params.sort;
+					if (params.cids) {
+						key = params.cids.map(function (cid) {
+							if (params.sort === 'recent') {
+								return 'cid:' + cid + ':tids:lastposttime';
+							} else if (params.sort === 'votes') {
+								return 'cid:' + cid + ':tids:votes';
+							} else if (params.sort === 'posts') {
+								return 'cid:' + cid + ':tids:posts';
+							}
+							return 'cid:' + cid + ':tids';
+						});
+					}
+
+					db.getSortedSetRevRange(key, 0, 199, next);
+				} else {
+					Topics.getLatestTidsFromSet('topics:tid', 0, -1, params.term, next);
 				}
-				db.getSortedSetRevRange(key, 0, 199, next);
 			},
 			function (tids, next) {
-				filterTids(tids, uid, filter, cid, next);
+				if (params.term !== 'alltime') {
+					sortTids(tids, params, next);
+				} else {
+					next(null, tids);
+				}
 			},
 			function (tids, next) {
-				recentTopics.topicCount = tids.length;
-				tids = tids.slice(start, stop !== -1 ? stop + 1 : undefined);
-				Topics.getTopicsByTids(tids, uid, next);
+				filterTids(tids, params.uid, params.filter, params.cids, next);
+			},
+		], callback);
+	}
+
+	function sortTids(tids, params, callback) {
+		async.waterfall([
+			function (next) {
+				Topics.getTopicsFields(tids, ['tid', 'lastposttime', 'upvotes', 'downvotes', 'postcount'], next);
+			},
+			function (topicData, next) {
+				var sortFn = sortRecent;
+				if (params.sort === 'posts') {
+					sortFn = sortPopular;
+				} else if (params.sort === 'votes') {
+					sortFn = sortVotes;
+				}
+				tids = topicData.sort(sortFn).map(function (topic) {
+					return topic && topic.tid;
+				});
+				next(null, tids);
+			},
+		], callback);
+	}
+
+	function sortRecent(a, b) {
+		return b.lastposttime - a.lastposttime;
+	}
+
+	function sortVotes(a, b) {
+		if (parseInt(a.votes, 10) !== parseInt(b.votes, 10)) {
+			return b.votes - a.votes;
+		}
+		return parseInt(b.postcount, 10) - parseInt(a.postcount, 10);
+	}
+
+	function sortPopular(a, b) {
+		if (parseInt(a.postcount, 10) !== parseInt(b.postcount, 10)) {
+			return b.postcount - a.postcount;
+		}
+		return parseInt(b.viewcount, 10) - parseInt(a.viewcount, 10);
+	}
+
+	function getTopics(tids, params, callback) {
+		async.waterfall([
+			function (next) {
+				tids = tids.slice(params.start, params.stop !== -1 ? params.stop + 1 : undefined);
+				Topics.getTopicsByTids(tids, params.uid, next);
 			},
 			function (topicData, next) {
 				topicData.forEach(function (topicObj, i) {
-					topicObj.index = start + i;
+					topicObj.index = params.start + i;
 				});
-				recentTopics.topics = topicData;
-				recentTopics.nextStart = stop + 1;
-				next(null, recentTopics);
+				next(null, topicData);
 			},
 		], callback);
+	}
+
+	Topics.getRecentTopics = function (cid, uid, start, stop, filter, callback) {
+		Topics.getSortedTopics({
+			cids: cid,
+			uid: uid,
+			start: start,
+			stop: stop,
+			filter: filter,
+			sort: 'recent',
+		}, callback);
 	};
 
-	function filterTids(tids, uid, filter, cid, callback) {
+	function filterTids(tids, uid, filter, cids, callback) {
 		async.waterfall([
 			function (next) {
 				if (filter === 'watched') {
@@ -95,10 +186,10 @@ module.exports = function (Topics) {
 				});
 			},
 			function (results, next) {
-				cid = cid && cid.map(String);
+				cids = cids && cids.map(String);
 				tids = results.topicData.filter(function (topic) {
 					if (topic && topic.cid) {
-						return results.ignoredCids.indexOf(topic.cid.toString()) === -1 && (!cid || (cid.length && cid.indexOf(topic.cid.toString()) !== -1));
+						return !results.ignoredCids.includes(topic.cid.toString()) && (!cids || (cids.length && cids.includes(topic.cid.toString())));
 					}
 					return false;
 				}).map(function (topic) {
diff --git a/src/topics/tools.js b/src/topics/tools.js
index bacfe20b4d..2ec4de624d 100644
--- a/src/topics/tools.js
+++ b/src/topics/tools.js
@@ -182,12 +182,11 @@ module.exports = function (Topics) {
 								async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:votes', tid),
 							], next);
 						} else {
-							var votes = (parseInt(topicData.upvotes, 10) || 0) - (parseInt(topicData.downvotes, 10) || 0);
 							async.parallel([
 								async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:pinned', tid),
 								async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', topicData.lastposttime, tid),
 								async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:posts', topicData.postcount, tid),
-								async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:votes', votes, tid),
+								async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:votes', topicData.votes, tid),
 							], next);
 						}
 					},
diff --git a/src/topics/top.js b/src/topics/top.js
index 2d86cc8eb3..0e4c3d991e 100644
--- a/src/topics/top.js
+++ b/src/topics/top.js
@@ -2,89 +2,15 @@
 
 'use strict';
 
-var async = require('async');
-
-var db = require('../database');
-var privileges = require('../privileges');
-var user = require('../user');
-var meta = require('../meta');
-
 module.exports = function (Topics) {
 	Topics.getTopTopics = function (cid, uid, start, stop, filter, callback) {
-		var topTopics = {
-			nextStart: 0,
-			topics: [],
-		};
-		if (cid && !Array.isArray(cid)) {
-			cid = [cid];
-		}
-		async.waterfall([
-			function (next) {
-				var key = 'topics:votes';
-				if (cid) {
-					key = cid.map(function (cid) {
-						return 'cid:' + cid + ':tids:votes';
-					});
-				}
-				db.getSortedSetRevRange(key, 0, 199, next);
-			},
-			function (tids, next) {
-				filterTids(tids, uid, filter, cid, next);
-			},
-			function (tids, next) {
-				topTopics.topicCount = tids.length;
-				tids = tids.slice(start, stop !== -1 ? stop + 1 : undefined);
-				Topics.getTopicsByTids(tids, uid, next);
-			},
-			function (topicData, next) {
-				topTopics.topics = topicData;
-				topTopics.nextStart = stop + 1;
-				next(null, topTopics);
-			},
-		], callback);
+		Topics.getSortedTopics({
+			cids: cid,
+			uid: uid,
+			start: start,
+			stop: stop,
+			filter: filter,
+			sort: 'votes',
+		}, callback);
 	};
-
-	function filterTids(tids, uid, filter, cid, callback) {
-		async.waterfall([
-			function (next) {
-				if (filter === 'watched') {
-					Topics.filterWatchedTids(tids, uid, next);
-				} else if (filter === 'new') {
-					Topics.filterNewTids(tids, uid, next);
-				} else if (filter === 'unreplied') {
-					Topics.filterUnrepliedTids(tids, next);
-				} else {
-					Topics.filterNotIgnoredTids(tids, uid, next);
-				}
-			},
-			function (tids, next) {
-				privileges.topics.filterTids('read', tids, uid, next);
-			},
-			function (tids, next) {
-				async.parallel({
-					ignoredCids: function (next) {
-						if (filter === 'watched' || parseInt(meta.config.disableRecentCategoryFilter, 10) === 1) {
-							return next(null, []);
-						}
-						user.getIgnoredCategories(uid, next);
-					},
-					topicData: function (next) {
-						Topics.getTopicsFields(tids, ['tid', 'cid'], next);
-					},
-				}, next);
-			},
-			function (results, next) {
-				cid = cid && cid.map(String);
-				tids = results.topicData.filter(function (topic) {
-					if (topic && topic.cid) {
-						return results.ignoredCids.indexOf(topic.cid.toString()) === -1 && (!cid || (cid.length && cid.indexOf(topic.cid.toString()) !== -1));
-					}
-					return false;
-				}).map(function (topic) {
-					return topic.tid;
-				});
-				next(null, tids);
-			},
-		], callback);
-	}
 };