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.
v1.18.x
barisusakli 11 years ago
parent c5b8a7b163
commit 7610c11cd1

@ -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"
}

@ -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();

@ -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);
$('#post-container li[data-pid]').each(function() {
var $this = $(this);
if(firstPid > parseInt($this.attr('data-pid'), 10)) {
after = $this;
if(after.next().length && after.next().hasClass('post-bar')) {
after = after.next();
}
} else {
return false;
var firstPostTimestamp = parseInt(data.posts[0].timestamp, 10);
var firstPostVotes = parseInt(data.posts[0].votes, 10);
var firstPostPid = data.posts[0].pid;
var firstReply = $('#post-container li.post-row[data-index!="0"]').first();
var lastReply = $('#post-container li.post-row[data-index!="0"]').last();
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;
}
});
if (!after) {
var firstPost = $('#post-container .post-row').first();
if(firstPid < parseInt(firstPost.attr('data-pid'), 10)) {
before = firstPost;
} 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 {
if (firstPostPid > firstReply.attr('data-pid')) {
before = firstReply;
} else if(firstPostPid <= firstReply.attr('data-pid')) {
after = lastReply;
}
}
}
}
@ -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;
}

@ -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);

@ -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]]'));

@ -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);
});
});

@ -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();

@ -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));

@ -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);
}

@ -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);

@ -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) {

@ -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;

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

@ -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);
});
});
});

@ -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,52 +29,57 @@ 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;
}
var pids = postData.map(function(post) {
return post.pid;
});
Topics.addPostData(postData, uid, callback);
});
};
async.parallel({
favourites : function(next) {
favourites.getFavouritesByPostIDs(pids, uid, next);
},
voteData : function(next) {
favourites.getVoteStatusByPostIDs(pids, uid, next);
},
userData : function(next) {
async.each(postData, posts.addUserInfoToPost, next);
},
privileges : function(next) {
async.map(pids, function (pid, next) {
privileges.posts.get(pid, uid, next);
}, next);
}
}, function(err, results) {
if(err) {
return callback(err);
}
Topics.addPostData = function(postData, uid, callback) {
var pids = postData.map(function(post) {
return post.pid;
});
for (var i = 0; i < postData.length; ++i) {
postData[i].deleted = parseInt(postData[i].deleted, 10) === 1;
postData[i].favourited = results.favourites[i];
postData[i].upvoted = results.voteData[i].upvoted;
postData[i].downvoted = results.voteData[i].downvoted;
postData[i].votes = postData[i].votes || 0;
postData[i].display_moderator_tools = results.privileges[i].editable;
postData[i].display_move_tools = results.privileges[i].move;
postData[i].selfPost = parseInt(uid, 10) === parseInt(postData[i].uid, 10);
if(postData[i].deleted && !results.privileges[i].view_deleted) {
postData[i].content = '[[topic:post_is_deleted]]';
}
async.parallel({
favourites : function(next) {
favourites.getFavouritesByPostIDs(pids, uid, next);
},
voteData : function(next) {
favourites.getVoteStatusByPostIDs(pids, uid, next);
},
userData : function(next) {
async.each(postData, posts.addUserInfoToPost, next);
},
privileges : function(next) {
async.map(pids, function (pid, next) {
privileges.posts.get(pid, uid, next);
}, next);
}
}, function(err, results) {
if(err) {
return callback(err);
}
for (var i = 0; i < postData.length; ++i) {
postData[i].deleted = parseInt(postData[i].deleted, 10) === 1;
postData[i].favourited = results.favourites[i];
postData[i].upvoted = results.voteData[i].upvoted;
postData[i].downvoted = results.voteData[i].downvoted;
postData[i].votes = postData[i].votes || 0;
postData[i].display_moderator_tools = results.privileges[i].editable;
postData[i].display_move_tools = results.privileges[i].move;
postData[i].selfPost = parseInt(uid, 10) === parseInt(postData[i].uid, 10);
if(postData[i].deleted && !results.privileges[i].view_deleted) {
postData[i].content = '[[topic:post_is_deleted]]';
}
}
callback(null, postData);
});
callback(null, postData);
});
};
@ -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) {

@ -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!!!

@ -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);
}

@ -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);
};
};

Loading…
Cancel
Save