backup database before upgrade!
upgrade script will take the first post of each topic and set the
`mainPid` property on the topic. then it will remove that pid from the
sorted sets for that topic, this was done to make alternative sorting
work.

added a new sorted set called `tid:<id>:posts:votes` that is used to
sort topic posts by vote count, the original sorted set `tid:<id>:posts`
is used to sort by oldest first or newest first.

the main post is added to the returned posts array on topic load and is
always at the top.
theme changes are minimal just a few new data properties on the posts
and the sorting dropdown.
hopefully didn't miss anything too critical.
This commit is contained in:
barisusakli
2014-06-06 22:12:14 -04:00
parent c5b8a7b163
commit 7610c11cd1
18 changed files with 291 additions and 84 deletions

View File

@@ -111,5 +111,10 @@
"more_users_and_guests": "%1 more user(s) and %2 guest(s)",
"more_users": "%1 more user(s)",
"more_guests": "%1 more guest(s)"
"more_guests": "%1 more guest(s)",
"sort_by": "Sort by",
"oldest_to_newest": "Oldest to Newest",
"newest_to_oldest": "Newest to Oldest",
"most_votes": "Most votes"
}

View File

@@ -8,14 +8,16 @@ define('forum/infinitescroll', function() {
var callback;
var previousScrollTop = 0;
var loadingMore = false;
var topOffset = 0;
scroll.init = function(cb) {
scroll.init = function(cb, _topOffest) {
callback = cb;
topOffset = _topOffest || 0;
$(window).off('scroll', onScroll).on('scroll', onScroll);
};
function onScroll() {
var top = $(window).height() * 0.1;
var top = $(window).height() * 0.1 + topOffset;
var bottom = ($(document).height() - $(window).height()) * 0.9;
var currentScrollTop = $(window).scrollTop();

View File

@@ -40,6 +40,8 @@ define('forum/topic', ['forum/pagination', 'forum/infinitescroll', 'forum/topic/
threadTools.init(tid, thread_state);
events.init();
handleSorting();
hidePostToolsForDeletedPosts();
enableInfiniteLoadingOrPagination();
@@ -77,6 +79,21 @@ define('forum/topic', ['forum/pagination', 'forum/infinitescroll', 'forum/topic/
socket.emit('topics.increaseViewCount', tid);
};
function handleSorting() {
var threadSort = $('.thread-sort');
threadSort.find('i').removeClass('fa-check');
var currentSetting = threadSort.find('a[data-sort="' + config.topicPostSort + '"]');
currentSetting.find('i').addClass('fa-check');
$('.thread-sort').on('click', 'a', function() {
var newSetting = $(this).attr('data-sort');
socket.emit('user.setTopicSort', newSetting, function(err) {
config.topicPostSort = newSetting;
ajaxify.go('topic/' + ajaxify.variables.get('topic_slug'));
});
});
}
function getPostIndex() {
var parts = window.location.pathname.split('/');
return parts[4] ? (parseInt(parts[4], 10) - 1) : '';
@@ -122,7 +139,7 @@ define('forum/topic', ['forum/pagination', 'forum/infinitescroll', 'forum/topic/
function enableInfiniteLoadingOrPagination() {
if(!config.usePagination) {
infinitescroll.init(loadMorePosts);
infinitescroll.init(loadMorePosts, $('#post-container .post-row[data-index="0"]').height());
} else {
navigator.hide();
@@ -283,25 +300,36 @@ define('forum/topic', ['forum/pagination', 'forum/infinitescroll', 'forum/topic/
before = null;
function findInsertionPoint() {
var firstPid = parseInt(data.posts[0].pid, 10);
var firstPostTimestamp = parseInt(data.posts[0].timestamp, 10);
var firstPostVotes = parseInt(data.posts[0].votes, 10);
var firstPostPid = data.posts[0].pid;
$('#post-container li[data-pid]').each(function() {
var $this = $(this);
var firstReply = $('#post-container li.post-row[data-index!="0"]').first();
var lastReply = $('#post-container li.post-row[data-index!="0"]').last();
if(firstPid > parseInt($this.attr('data-pid'), 10)) {
after = $this;
if(after.next().length && after.next().hasClass('post-bar')) {
after = after.next();
if (config.topicPostSort === 'oldest_to_newest') {
if (firstPostTimestamp < parseInt(firstReply.attr('data-timestamp'), 10)) {
before = firstReply;
} else if(firstPostTimestamp >= parseInt(lastReply.attr('data-timestamp'), 10)) {
after = lastReply;
}
} else if(config.topicPostSort === 'newest_to_oldest') {
if (firstPostTimestamp > parseInt(firstReply.attr('data-timestamp'), 10)) {
before = firstReply;
} else if(firstPostTimestamp <= parseInt(lastReply.attr('data-timestamp'), 10)) {
after = lastReply;
}
} else if(config.topicPostSort === 'most_votes') {
if (firstPostVotes > parseInt(firstReply.attr('data-votes'), 10)) {
before = firstReply;
} else if(firstPostVotes < parseInt(firstReply.attr('data-votes'), 10)) {
after = lastReply;
} else {
return false;
if (firstPostPid > firstReply.attr('data-pid')) {
before = firstReply;
} else if(firstPostPid <= firstReply.attr('data-pid')) {
after = lastReply;
}
});
if (!after) {
var firstPost = $('#post-container .post-row').first();
if(firstPid < parseInt(firstPost.attr('data-pid'), 10)) {
before = firstPost;
}
}
}
@@ -373,7 +401,7 @@ define('forum/topic', ['forum/pagination', 'forum/infinitescroll', 'forum/topic/
return;
}
infinitescroll.calculateAfter(direction, '#post-container .post-row', config.postsPerPage, function(after, offset, el) {
infinitescroll.calculateAfter(direction, '#post-container .post-row[data-index!="0"]', config.postsPerPage, function(after, offset, el) {
loadPostsAfter(after, function() {
if (direction < 0 && el) {
Topic.scrollToPost(el.attr('data-index'), false, 0, offset);
@@ -384,7 +412,7 @@ define('forum/topic', ['forum/pagination', 'forum/infinitescroll', 'forum/topic/
function loadPostsAfter(after, callback) {
var tid = ajaxify.variables.get('topic_id');
if (!utils.isNumber(tid) || !utils.isNumber(after) || (after === 0 && $('#post-container li.post-row[data-index="0"]').length)) {
if (!utils.isNumber(tid) || !utils.isNumber(after) || (after === 0 && $('#post-container li.post-row[data-index="1"]').length)) {
return;
}

View File

@@ -42,6 +42,7 @@ apiController.getConfig = function(req, res, next) {
config.isLoggedIn = !!req.user;
config['cache-buster'] = meta.config['cache-buster'] || '';
config.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1;
config.topicPostSort = meta.config.topicPostSort || 'oldest_to_newest';
config.version = pkg.version;
if (!req.user) {
@@ -64,6 +65,7 @@ apiController.getConfig = function(req, res, next) {
config.notificationSounds = settings.notificationSounds;
config.defaultLang = settings.language || config.defaultLang;
config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab;
config.topicPostSort = settings.topicPostSort || config.topicPostSort;
if (res.locals.isAPI) {
res.json(200, config);

View File

@@ -44,7 +44,17 @@ topicsController.get = function(req, res, next) {
var start = (page - 1) * settings.postsPerPage + postIndex,
end = start + settings.postsPerPage - 1;
topics.getTopicWithPosts(tid, uid, start, end, function (err, topicData) {
var set = 'tid:' + tid + ':posts',
reverse = false;
if (settings.topicPostSort === 'newest_to_oldest') {
reverse = true;
} else if (settings.topicPostSort === 'most_votes') {
reverse = true;
set = 'tid:' + tid + ':posts:votes';
}
topics.getTopicWithPosts(tid, set, uid, start, end, reverse, function (err, topicData) {
if (topicData) {
if (parseInt(topicData.deleted, 10) === 1 && !userPrivileges.view_deleted) {
return next(new Error('[[error:no-topic]]'));

View File

@@ -88,7 +88,8 @@ var async = require('async'),
return callback(err);
}
var voteCount = parseInt(results.upvotes, 10) - parseInt(results.downvotes, 10);
posts.setPostField(pid, 'votes', voteCount, function(err) {
posts.updatePostVoteCount(pid, voteCount, function(err) {
callback(err, voteCount);
});
});

View File

@@ -85,9 +85,10 @@ middleware.checkPostIndex = function(req, res, next) {
return next(err);
}
var postIndex = parseInt(req.params.post_index, 10);
postCount = parseInt(postCount, 10) + 1;
if (postIndex > postCount) {
return res.locals.isAPI ? res.json(302, '/topic/' + req.params.topic_id + '/' + req.params.slug + '/' + postCount) : res.redirect('/topic/' + req.params.topic_id + '/' + req.params.slug + '/' + postCount);
} else if (postIndex < 1) {
} else if (postIndex <= 1) {
return res.locals.isAPI ? res.json(302, '/topic/' + req.params.topic_id + '/' + req.params.slug) : res.redirect('/topic/' + req.params.topic_id + '/' + req.params.slug);
}
next();

View File

@@ -90,8 +90,8 @@ var db = require('./database'),
], callback);
};
Posts.getPostsByTid = function(tid, start, end, reverse, callback) {
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange']('tid:' + tid + ':posts', start, end, function(err, pids) {
Posts.getPostsByTid = function(tid, set, start, end, reverse, callback) {
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, end, function(err, pids) {
if(err) {
return callback(err);
}
@@ -157,8 +157,6 @@ var db = require('./database'),
});
};
Posts.getRecentPosts = function(uid, start, stop, term, callback) {
var terms = {
day: 86400000,
@@ -469,7 +467,9 @@ var db = require('./database'),
return callback(err);
}
db.sortedSetRank('tid:' + tid + ':posts', pid, callback);
db.sortedSetRank('tid:' + tid + ':posts', pid, function(err, index) {
callback(err, parseInt(index, 10) + 1);
});
});
};
@@ -482,5 +482,28 @@ var db = require('./database'),
});
};
Posts.updatePostVoteCount = function(pid, voteCount, callback) {
async.parallel([
function(next) {
Posts.getPostField(pid, 'tid', function(err, tid) {
if (err) {
return next(err);
}
topics.getTopicField(tid, 'mainPid', function(err, mainPid) {
if (err) {
return next(err);
}
if (parseInt(mainPid, 10) === parseInt(pid, 10)) {
return next();
}
db.sortedSetAdd('tid:' + tid + ':posts:votes', voteCount, pid, next);
});
});
},
function(next) {
Posts.setPostField(pid, 'votes', voteCount, next);
}
], callback);
};
}(exports));

View File

@@ -40,7 +40,7 @@ function hasPrivileges(method, id, req, res, next) {
function generateForTopic(req, res, next) {
var tid = req.params.topic_id;
var uid = req.user ? req.user.uid : 0;
topics.getTopicWithPosts(tid, uid, 0, 25, function (err, topicData) {
topics.getTopicWithPosts(tid, 'tid:' + tid + ':posts', uid, 0, 25, false, function (err, topicData) {
if (err) {
return next(err);
}

View File

@@ -314,12 +314,22 @@ SocketTopics.loadMore = function(socket, data, callback) {
return callback(err);
}
var start = parseInt(data.after, 10),
var start = Math.max(parseInt(data.after, 10) - 1, 0),
end = start + settings.postsPerPage - 1;
var set = 'tid:' + data.tid + ':posts',
reverse = false;
if (settings.topicPostSort === 'newest_to_oldest') {
reverse = true;
} else if (settings.topicPostSort === 'most_votes') {
reverse = true;
set = 'tid:' + data.tid + ':posts:votes';
}
async.parallel({
posts: function(next) {
topics.getTopicPosts(data.tid, start, end, socket.uid, false, next);
topics.getTopicPosts(data.tid, set, start, end, socket.uid, reverse, next);
},
privileges: function(next) {
privileges.topics.get(data.tid, socket.uid, next);

View File

@@ -178,6 +178,12 @@ SocketUser.saveSettings = function(socket, data, callback) {
}
};
SocketUser.setTopicSort = function(socket, sort, callback) {
if(socket.uid) {
user.setSetting(socket.uid, 'topicPostSort', sort, callback);
}
};
SocketUser.getOnlineUsers = function(socket, data, callback) {
var returnData = {};
if(!data) {

View File

@@ -262,7 +262,7 @@ var async = require('async'),
});
};
Topics.getTopicWithPosts = function(tid, uid, start, end, callback) {
Topics.getTopicWithPosts = function(tid, set, uid, start, end, reverse, callback) {
Topics.getTopicData(tid, function(err, topicData) {
if (err || !topicData) {
return callback(err || new Error('[[error:no-topic]]'));
@@ -270,7 +270,7 @@ var async = require('async'),
async.parallel({
posts: function(next) {
Topics.getTopicPosts(tid, start, end, uid, false, next);
Topics.getTopicPosts(tid, set, start, end, uid, reverse, next);
},
category: function(next) {
Topics.getCategoryData(tid, next);
@@ -283,6 +283,26 @@ var async = require('async'),
},
tags: function(next) {
Topics.getTopicTagsObjects(tid, next);
},
mainPost: function(next) {
Topics.getTopicField(tid, 'mainPid', function(err, mainPid) {
if (err) {
return next(err);
}
if (!parseInt(mainPid, 10)) {
return next(null, []);
}
posts.getPostsByPids([mainPid], function(err, postData) {
if (err) {
return next(err);
}
if (!Array.isArray(postData) || !postData.length) {
return next(null, []);
}
postData[0].index = 0;
Topics.addPostData(postData, uid, next);
});
});
}
}, function(err, results) {
if (err) {
@@ -290,7 +310,7 @@ var async = require('async'),
}
topicData.category = results.category;
topicData.posts = results.posts;
topicData.posts = results.mainPost.concat(results.posts);
topicData.tags = results.tags;
topicData.thread_tools = results.threadTools;
topicData.pageCount = results.pageCount;

View File

@@ -37,6 +37,7 @@ module.exports = function(Topics) {
'tid': tid,
'uid': uid,
'cid': cid,
'mainPid': 0,
'title': title,
'slug': slug,
'timestamp': timestamp,

View File

@@ -71,7 +71,7 @@ module.exports = function(Topics) {
return callback(err || new Error('[[error:no-topic]]'));
}
posts.getPostFields(pid, ['deleted', 'tid', 'timestamp'], function(err, postData) {
posts.getPostFields(pid, ['deleted', 'tid', 'timestamp', 'votes'], function(err, postData) {
if(err) {
return callback(err);
}
@@ -91,7 +91,7 @@ module.exports = function(Topics) {
}
posts.setPostField(pid, 'tid', tid);
Topics.addPostToTopic(tid, pid, postData.timestamp, callback);
Topics.addPostToTopic(tid, pid, postData.timestamp, postData.votes, callback);
});
});
});

View File

@@ -15,13 +15,13 @@ module.exports = function(Topics) {
Topics.onNewPostMade = function(postData) {
Topics.increasePostCount(postData.tid);
Topics.updateTimestamp(postData.tid, postData.timestamp);
Topics.addPostToTopic(postData.tid, postData.pid, postData.timestamp);
Topics.addPostToTopic(postData.tid, postData.pid, postData.timestamp, 0);
};
emitter.on('event:newpost', Topics.onNewPostMade);
Topics.getTopicPosts = function(tid, start, end, uid, reverse, callback) {
posts.getPostsByTid(tid, start, end, reverse, function(err, postData) {
Topics.getTopicPosts = function(tid, set, start, end, uid, reverse, callback) {
posts.getPostsByTid(tid, set, start, end, reverse, function(err, postData) {
if(err) {
return callback(err);
}
@@ -29,11 +29,17 @@ module.exports = function(Topics) {
if (Array.isArray(postData) && !postData.length) {
return callback(null, []);
}
start = parseInt(start, 10);
for(var i=0; i<postData.length; ++i) {
postData[i].index = start + i;
postData[i].index = start + i + 1;
}
Topics.addPostData(postData, uid, callback);
});
};
Topics.addPostData = function(postData, uid, callback) {
var pids = postData.map(function(post) {
return post.pid;
});
@@ -75,7 +81,6 @@ module.exports = function(Topics) {
callback(null, postData);
});
});
};
Topics.getLatestUndeletedPost = function(tid, callback) {
@@ -108,8 +113,21 @@ module.exports = function(Topics) {
});
};
Topics.addPostToTopic = function(tid, pid, timestamp, callback) {
db.sortedSetAdd('tid:' + tid + ':posts', timestamp, pid, callback);
Topics.addPostToTopic = function(tid, pid, timestamp, votes, callback) {
Topics.getTopicField(tid, 'mainPid', function(err, mainPid) {
if (!parseInt(mainPid, 10)) {
Topics.setTopicField(tid, 'mainPid', pid, callback);
} else {
async.parallel([
function(next) {
db.sortedSetAdd('tid:' + tid + ':posts', timestamp, pid, next);
},
function(next) {
db.sortedSetAdd('tid:' + tid + ':posts:votes', votes, pid, next);
}
], callback);
}
});
};
Topics.removePostFromTopic = function(tid, pid, callback) {

View File

@@ -19,7 +19,7 @@ var db = require('./database'),
schemaDate, thisSchemaDate,
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
latestSchema = Date.UTC(2014, 4, 22);
latestSchema = Date.UTC(2014, 5, 6);
Upgrade.check = function(callback) {
db.get('schemaDate', function(err, value) {
@@ -733,6 +733,74 @@ Upgrade.upgrade = function(callback) {
winston.info('[2014/5/16] Tags upgrade - skipped');
next();
}
},
function(next) {
thisSchemaDate = Date.UTC(2014, 5, 6);
if (schemaDate < thisSchemaDate) {
db.getSortedSetRange('topics:tid', 0, -1, function(err, tids) {
function upgradeTopic(tid, callback) {
Topics.getTopicField(tid, 'mainPid', function(err, mainPid) {
if (err) {
return callback(err);
}
db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, function(err, pids) {
if (err) {
return callback(err);
}
if (!Array.isArray(pids) || !pids.length) {
return callback();
}
if (!parseInt(mainPid, 10)) {
mainPid = pids[0];
pids.splice(0, 1);
Topics.setTopicField(tid, 'mainPid', mainPid);
db.sortedSetRemove('tid:' + tid + ':posts', mainPid);
db.sortedSetRemove('tid:' + tid + ':posts:votes', mainPid);
}
if (!pids.length) {
return callback();
}
async.each(pids, function(pid, next) {
Posts.getPostField(pid, 'votes', function(err, votes) {
if (err) {
return next(err);
}
db.sortedSetAdd('tid:' + tid + ':posts:votes', votes ? votes : 0, pid, next);
});
}, callback);
});
});
}
if (err) {
return next(err);
}
if (!Array.isArray(tids) || !tids.length) {
winston.info('[2014/6/6] Skipping topic upgrade');
return Upgrade.update(thisSchemaDate, next);
}
async.each(tids, upgradeTopic, function(err) {
if (err) {
winston.error('[2014/6/6] Error encountered while upgrading topics');
return next(err);
}
winston.info('[2014/6/6] Topics upgraded.');
Upgrade.update(thisSchemaDate, next);
});
});
} else {
winston.info('[2014/6/6] Topic upgrade - skipped');
next();
}
}
// Add new schema updates here
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 22!!!

View File

@@ -66,7 +66,14 @@ module.exports = function(User) {
return callback(err);
}
db.sortedSetRemove('tid:' + postData.tid + ':posts', pid, function(err) {
async.parallel([
function(next) {
db.sortedSetRemove('tid:' + postData.tid + ':posts', pid, next);
},
function(next) {
db.sortedSetRemove('tid:' + postData.tid + ':posts:votes', pid, next);
}
], function(err) {
if (err) {
return callback(err);
}

View File

@@ -32,6 +32,7 @@ module.exports = function(User) {
settings.postsPerPage = settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : parseInt(meta.config.postsPerPage, 10) || 10;
settings.notificationSounds = settings.notificationSounds ? parseInt(settings.notificationSounds, 10) === 1 : true;
settings.language = settings.language || meta.config.defaultLang || 'en_GB';
settings.topicPostSort = settings.topicPostSort || meta.config.topicPostSort || 'oldest_to_newest';
callback(null, settings);
});
});
@@ -82,4 +83,8 @@ module.exports = function(User) {
language: data.language || meta.config.defaultLang
}, callback);
};
User.setSetting = function(uid, key, value, callback) {
db.setObjectField('user:' + uid + ':settings', key, value, callback);
};
};