diff --git a/app.js b/app.js index e41c70349b..d83bb95d36 100644 --- a/app.js +++ b/app.js @@ -58,7 +58,7 @@ if(os.platform() === 'linux') { } // Log GNU copyright info along with server info -winston.info('NodeBB v' + pkg.version + ' Copyright (C) 2013-2014 DesignCreatePlay Inc.'); +winston.info('NodeBB v' + pkg.version + ' Copyright (C) 2013-2014 NodeBB Inc.'); winston.info('This program comes with ABSOLUTELY NO WARRANTY.'); winston.info('This is free software, and you are welcome to redistribute it under certain conditions.'); winston.info(''); diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index 8958beb1dc..6bbb8ffe3e 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -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" } diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index b1d63f7141..04e039d1f2 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -60,7 +60,7 @@ var ajaxify = ajaxify || {}; // Remove trailing slash url = url.replace(/\/$/, ""); - url = removeRelativePath(url); + url = ajaxify.removeRelativePath(url); var tpl_url = ajaxify.getTemplateMapping(url); @@ -122,12 +122,12 @@ var ajaxify = ajaxify || {}; return false; }; - function removeRelativePath(url) { - if (url.indexOf(RELATIVE_PATH.slice(1)) !== -1) { + ajaxify.removeRelativePath = function(url) { + if (url.indexOf(RELATIVE_PATH.slice(1)) === 0) { url = url.slice(RELATIVE_PATH.length); } return url; - } + }; ajaxify.refresh = function() { ajaxify.go(ajaxify.currentPage); @@ -186,7 +186,7 @@ var ajaxify = ajaxify || {}; }; ajaxify.loadData = function(url, callback) { - url = removeRelativePath(url); + url = ajaxify.removeRelativePath(url); $(window).trigger('action:ajaxify.loadingData', {url: url}); diff --git a/public/src/app.js b/public/src/app.js index fda565a0a1..242f874e72 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -66,7 +66,7 @@ var socket, }); } }); - + $(window).trigger('action:reconnected'); setTimeout(function() { @@ -162,7 +162,7 @@ var socket, socket.emit('tools.log', arguments); }; } - + app.cacheBuster = config['cache-buster']; } } @@ -516,9 +516,7 @@ var socket, tpl_url = ajaxify.getTemplateMapping(url), $window = $(window); - if (url.indexOf(RELATIVE_PATH.slice(1)) !== -1) { - url = url.slice(RELATIVE_PATH.length); - } + url = ajaxify.removeRelativePath(url); ajaxify.widgets.render(tpl_url, url); diff --git a/public/src/forum/infinitescroll.js b/public/src/forum/infinitescroll.js index a496ff02e9..053635f312 100644 --- a/public/src/forum/infinitescroll.js +++ b/public/src/forum/infinitescroll.js @@ -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(); diff --git a/public/src/forum/topic.js b/public/src/forum/topic.js index 319feef24f..7b75f00371 100644 --- a/public/src/forum/topic.js +++ b/public/src/forum/topic.js @@ -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; } diff --git a/public/src/modules/composer/drafts.js b/public/src/modules/composer/drafts.js index d355cfa054..27370607ca 100644 --- a/public/src/modules/composer/drafts.js +++ b/public/src/modules/composer/drafts.js @@ -5,23 +5,28 @@ define('composer/drafts', function() { var drafts = {}; - + var saveThrottleId; var saving = false; drafts.init = function(postContainer, postData) { - var saveThrottle; + var bodyEl = postContainer.find('textarea'); bodyEl.on('keyup', function() { - if (saveThrottle) { - clearTimeout(saveThrottle); - } + resetTimeout(); - saveThrottle = setTimeout(function() { + saveThrottleId = setTimeout(function() { saveDraft(postContainer, postData); }, 1000); }); }; + function resetTimeout() { + if (saveThrottleId) { + clearTimeout(saveThrottleId); + saveThrottleId = 0; + } + } + drafts.getDraft = function(save_id) { return localStorage.getItem(save_id); }; @@ -40,6 +45,7 @@ define('composer/drafts', function() { } drafts.removeDraft = function(save_id) { + resetTimeout(); return localStorage.removeItem(save_id); }; diff --git a/src/controllers/api.js b/src/controllers/api.js index d83cdab13d..ef355cbd51 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -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); diff --git a/src/controllers/topics.js b/src/controllers/topics.js index bed49056be..9fce84d994 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -44,12 +44,25 @@ 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]]')); } topicData.currentPage = page; + if(page > 1) { + topicData.posts.splice(0, 1); + } } next(err, topicData); }); diff --git a/src/favourites.js b/src/favourites.js index 0398614b9f..9e76887faa 100644 --- a/src/favourites.js +++ b/src/favourites.js @@ -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); }); }); diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 6d0a172414..4c8b6a9e5f 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -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(); diff --git a/src/postTools.js b/src/postTools.js index 20e8cc272c..f496e640bf 100644 --- a/src/postTools.js +++ b/src/postTools.js @@ -19,16 +19,8 @@ var winston = require('winston'), (function(PostTools) { PostTools.isMain = function(pid, tid, callback) { - db.getSortedSetRange('tid:' + tid + ':posts', 0, 0, function(err, pids) { - if(err) { - return callback(err); - } - - if(!Array.isArray(pids) || !pids.length) { - return callback(null, false); - } - - callback(null, parseInt(pids[0], 10) === parseInt(pid, 10)); + topics.getTopicField(tid, 'mainPid', function(err, mainPid) { + callback(err, parseInt(pid, 10) === parseInt(mainPid, 10)); }); }; diff --git a/src/posts.js b/src/posts.js index 69f555a05c..5861a58042 100644 --- a/src/posts.js +++ b/src/posts.js @@ -24,6 +24,8 @@ var db = require('./database'), (function(Posts) { var customUserInfo = {}; + require('./posts/delete')(Posts); + Posts.create = function(data, callback) { var uid = data.uid, tid = data.tid, @@ -90,8 +92,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 +159,6 @@ var db = require('./database'), }); }; - - Posts.getRecentPosts = function(uid, start, stop, term, callback) { var terms = { day: 86400000, @@ -469,7 +469,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 +484,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)); diff --git a/src/posts/delete.js b/src/posts/delete.js new file mode 100644 index 0000000000..72b420e107 --- /dev/null +++ b/src/posts/delete.js @@ -0,0 +1,9 @@ + + +module.exports = function(Posts) { + + Posts.delete = function(pid, callback) { + + }; + +}; \ No newline at end of file diff --git a/src/routes/api.js b/src/routes/api.js index d90951b73a..354c3d640b 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -134,15 +134,33 @@ function getModerators(req, res, next) { }); } +var templatesListingCache = []; + function getTemplatesListing(req, res, next) { - utils.walk(nconf.get('views_dir'), function (err, data) { - data = data - .filter(function(value, index, self) { + if (templatesListingCache.length) { + return res.json(templatesListingCache); + } + + async.parallel({ + views: function(next) { + utils.walk(nconf.get('views_dir'), next); + }, + extended: function(next) { + plugins.fireHook('filter:templates.get_virtual', [], next); + } + }, function(err, results) { + if (err) { + return next(err); + } + var data = []; + data = results.views.filter(function(value, index, self) { return self.indexOf(value) === index; }).map(function(el) { return el.replace(nconf.get('views_dir') + '/', ''); }); + data = data.concat(results.extended); + templatesListingCache = data; res.json(data); }); } diff --git a/src/routes/feeds.js b/src/routes/feeds.js index 624c06e9a1..30b3cb6c4c 100644 --- a/src/routes/feeds.js +++ b/src/routes/feeds.js @@ -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); } diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index b7c84ff127..466859038a 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -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); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 4747c24d2f..5cf805362b 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -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) { diff --git a/src/topics.js b/src/topics.js index 05b57f5d33..0b225092d4 100644 --- a/src/topics.js +++ b/src/topics.js @@ -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; diff --git a/src/topics/create.js b/src/topics/create.js index 7bf23b4bed..a6b3ef353a 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -37,6 +37,7 @@ module.exports = function(Topics) { 'tid': tid, 'uid': uid, 'cid': cid, + 'mainPid': 0, 'title': title, 'slug': slug, 'timestamp': timestamp, diff --git a/src/topics/fork.js b/src/topics/fork.js index 35880cc2ea..4d276f16fb 100644 --- a/src/topics/fork.js +++ b/src/topics/fork.js @@ -15,15 +15,15 @@ var async = require('async'), module.exports = function(Topics) { Topics.createTopicFromPosts = function(uid, title, pids, callback) { - if(title) { + if (title) { title = title.trim(); } - if(!title) { + if (!title) { return callback(new Error('[[error:invalid-title]]')); } - if(!pids || !pids.length) { + if (!pids || !pids.length) { return callback(new Error('[[error:invalid-pid]]')); } @@ -39,12 +39,12 @@ module.exports = function(Topics) { } }, function(err, results) { Topics.create({uid: results.postData.uid, title: title, cid: results.cid}, function(err, tid) { - if(err) { + if (err) { return callback(err); } async.eachSeries(pids, move, function(err) { - if(err) { + if (err) { return callback(err); } @@ -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); }); }); }); diff --git a/src/topics/posts.js b/src/topics/posts.js index 13cd8ca37c..2819c6e647 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -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