diff --git a/public/src/app.js b/public/src/app.js index 4725ca9866..6928ab3197 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -569,6 +569,7 @@ var socket, createHeaderTooltips(); ajaxify.variables.parse(); + ajaxify.currentPage = tpl_url; app.processPage(); ajaxify.widgets.render(tpl_url, url); diff --git a/public/src/forum/category.js b/public/src/forum/category.js index 8acd9543ce..e834156a3a 100644 --- a/public/src/forum/category.js +++ b/public/src/forum/category.js @@ -1,13 +1,13 @@ "use strict"; /* global define, config, templates, app, utils, ajaxify, socket, translator */ -define(['composer', 'forum/pagination', 'share', 'navigator'], function(composer, pagination, share, navigator) { +define(['composer', 'forum/pagination', 'share', 'navigator', 'forum/categoryTools'], function(composer, pagination, share, navigator, categoryTools) { var Category = {}, loadingMoreTopics = false; $(window).on('action:ajaxify.start', function(ev, data) { - if(data.url.indexOf('category') !== 0) { + if(data && data.url.indexOf('category') !== 0) { navigator.hide(); } }); @@ -24,11 +24,14 @@ define(['composer', 'forum/pagination', 'share', 'navigator'], function(composer }); ajaxify.register_events([ - 'event:new_topic' + 'event:new_topic', 'event:topic_deleted', 'event:topic_restored', 'event:topic_locked', + 'event:topic_unlocked', 'event:topic_pinned', 'event:topic_unpinned', 'event:topic_moved' ]); socket.on('event:new_topic', Category.onNewTopic); + categoryTools.init(cid); + enableInfiniteLoading(); if (!config.usePagination) { diff --git a/public/src/forum/categoryTools.js b/public/src/forum/categoryTools.js new file mode 100644 index 0000000000..34608f6d63 --- /dev/null +++ b/public/src/forum/categoryTools.js @@ -0,0 +1,168 @@ + +'use strict'; + +/* globals define, app, translator, socket, bootbox */ + + +define(['forum/topic/move', 'topicSelect'], function(move, topicSelect) { + + var CategoryTools = {}; + + CategoryTools.init = function(cid) { + CategoryTools.cid = cid; + + topicSelect.init(onTopicSelect); + + $('.delete_thread').on('click', function(e) { + var tids = topicSelect.getSelectedTids(); + + if (tids.length) { + var command = isAny(isTopicDeleted, tids) ? 'restore' : 'delete'; + + translator.translate('[[topic:thread_tools.' + command + '_confirm]]', function(msg) { + bootbox.confirm(msg, function(confirm) { + if (!confirm) { + return; + } + + socket.emit('topics.' + command, tids, onCommandComplete); + }); + }); + } + + return false; + }); + + $('.lock_thread').on('click', function(e) { + var tids = topicSelect.getSelectedTids(); + if (tids.length) { + socket.emit(isAny(isTopicLocked, tids) ? 'topics.unlock' : 'topics.lock', tids, onCommandComplete); + } + + return false; + }); + + $('.pin_thread').on('click', function(e) { + var tids = topicSelect.getSelectedTids(); + if (tids.length) { + socket.emit(isAny(isTopicPinned, tids) ? 'topics.unpin' : 'topics.pin', tids, onCommandComplete); + } + return false; + }); + + $('.markAsUnreadForAll').on('click', function() { + var tids = topicSelect.getSelectedTids(); + if (tids.length) { + socket.emit('topics.markAsUnreadForAll', tids, function(err) { + if(err) { + return app.alertError(err.message); + } + app.alertSuccess('[[topic:markAsUnreadForAll.success]]'); + + onCommandComplete(); + }); + } + + return false; + }); + + $('.move_thread').on('click', function() { + var tids = topicSelect.getSelectedTids(); + console.log(tids); + if (tids.length) { + move.init(tids, cid, onCommandComplete); + } + return false; + }); + + + socket.on('event:topic_deleted', setDeleteState); + socket.on('event:topic_restored', setDeleteState); + socket.on('event:topic_locked', setLockedState); + socket.on('event:topic_unlocked', setLockedState); + socket.on('event:topic_pinned', setPinnedState); + socket.on('event:topic_unpinned', setPinnedState); + socket.on('event:topic_moved', onTopicMoved); + }; + + function closeDropDown() { + $('.thread-tools.open').find('.dropdown-toggle').trigger('click'); + } + + function onCommandComplete(err) { + if (err) { + return app.alertError(err.message); + } + closeDropDown(); + topicSelect.unselectAll(); + } + + function onTopicSelect() { + var tids = topicSelect.getSelectedTids(); + var isAnyDeleted = isAny(isTopicDeleted, tids); + var isAnyPinned = isAny(isTopicPinned, tids); + var isAnyLocked = isAny(isTopicLocked, tids); + + translator.translate(' [[topic:thread_tools.' + (isAnyDeleted ? 'restore' : 'delete') + ']]', function(translated) { + $('.delete_thread span').html(translated); + }); + + translator.translate(' [[topic:thread_tools.' + (isAnyPinned ? 'unpin' : 'pin') + ']]', function(translated) { + $('.pin_thread').html(translated); + }); + + translator.translate(' [[topic:thread_tools.' + (isAnyLocked ? 'un': '') + 'lock]]', function(translated) { + $('.lock_thread').html(translated); + }); + } + + function isAny(method, tids) { + for(var i=0; i'); - - moveTopic(); + app.alertSuccess('[[topic:topic_move_success, ' + targetCategoryLabel + ']]'); + if (typeof Move.onComplete === 'function') { + Move.onComplete(); } - } + }); + } - function moveTopic() { - socket.emit('topics.move', { - tid: tid, - cid: targetCid - }, function(err) { - modal.modal('hide'); - if(err) { - return app.alertError(err.message); - } + function renderCategories(categories) { + var categoriesEl = modal.find('.category-list'), + info; - app.alertSuccess('[[topic:topic_move_success, ' + targetCategoryLabel + ']]'); - }); - } - - function renderCategories(categories) { - var categoriesEl = modal.find('.category-list'), - info; - - for (var x = 0; x < categories.length; ++x) { - info = categories[x]; - $('
  • ') - .css({background: info.bgColor, color: info.color || '#fff'}) - .addClass(info.disabled === '1' ? ' disabled' : '') - .attr('data-cid', info.cid) - .html(' ' + info.name) - .appendTo(categoriesEl); + for (var x = 0; x < categories.length; ++x) { + info = categories[x]; + if(parseInt(info.cid, 10) === parseInt(Move.currentCid, 10)) { + continue; } - - $('#categories-loading').remove(); + $('
  • ') + .css({background: info.bgColor, color: info.color || '#fff'}) + .addClass(info.disabled === '1' ? ' disabled' : '') + .attr('data-cid', info.cid) + .html(' ' + info.name) + .appendTo(categoriesEl); } - }; + + $('#categories-loading').remove(); + } return Move; }); diff --git a/public/src/forum/topic/threadTools.js b/public/src/forum/topic/threadTools.js index 186bf682c6..ec03d81200 100644 --- a/public/src/forum/topic/threadTools.js +++ b/public/src/forum/topic/threadTools.js @@ -18,7 +18,7 @@ define(['forum/topic/fork', 'forum/topic/move'], function(fork, move) { translator.translate('[[topic:thread_tools.' + command + '_confirm]]', function(msg) { bootbox.confirm(msg, function(confirm) { if (confirm) { - socket.emit('topics.' + command, tid); + socket.emit('topics.' + command, [tid]); } }); }); @@ -27,18 +27,18 @@ define(['forum/topic/fork', 'forum/topic/move'], function(fork, move) { }); $('.lock_thread').on('click', function(e) { - socket.emit(threadState.locked !== '1' ? 'topics.lock' : 'topics.unlock', tid); + socket.emit(threadState.locked !== '1' ? 'topics.lock' : 'topics.unlock', [tid]); return false; }); $('.pin_thread').on('click', function(e) { - socket.emit(threadState.pinned !== '1' ? 'topics.pin' : 'topics.unpin', tid); + socket.emit(threadState.pinned !== '1' ? 'topics.pin' : 'topics.unpin', [tid]); return false; }); $('.markAsUnreadForAll').on('click', function() { var btn = $(this); - socket.emit('topics.markAsUnreadForAll', tid, function(err) { + socket.emit('topics.markAsUnreadForAll', [tid], function(err) { if(err) { return app.alertError(err.message); } @@ -48,7 +48,10 @@ define(['forum/topic/fork', 'forum/topic/move'], function(fork, move) { return false; }); - move.init(tid); + $('.move_thread').on('click', function(e) { + move.init([tid], ajaxify.variables.get('category_id')); + return false; + }); fork.init(); } diff --git a/public/src/forum/unread.js b/public/src/forum/unread.js index cdfe192df3..d7b31b7eec 100644 --- a/public/src/forum/unread.js +++ b/public/src/forum/unread.js @@ -2,7 +2,7 @@ /* globals define, app, socket */ -define(['forum/recent'], function(recent) { +define(['forum/recent', 'topicSelect'], function(recent, topicSelect) { var Unread = {}, loadingMoreTopics = false; @@ -16,14 +16,7 @@ define(['forum/recent'], function(recent) { recent.watchForNewPosts(); $('#markSelectedRead').on('click', function() { - function getSelectedTids() { - var tids = []; - $('#topics-container .category-item.selected').each(function() { - tids.push($(this).attr('data-tid')); - }); - return tids; - } - var tids = getSelectedTids(); + var tids = topicSelect.getSelectedTids(); if(!tids.length) { return; } @@ -77,14 +70,7 @@ define(['forum/recent'], function(recent) { socket.emit('categories.get', onCategoriesLoaded); - $('#topics-container').on('click', '.select', function() { - var select = $(this); - var isChecked = !select.hasClass('fa-square-o'); - - select.toggleClass('fa-check-square-o', !isChecked); - select.toggleClass('fa-square-o', isChecked); - select.parents('.category-item').toggleClass('selected', !isChecked); - }); + topicSelect.init(); if ($("body").height() <= $(window).height() && $('#topics-container').children().length >= 20) { $('#load-more-btn').show(); diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js new file mode 100644 index 0000000000..4fbbc38f12 --- /dev/null +++ b/public/src/modules/topicSelect.js @@ -0,0 +1,36 @@ +'use strict'; + +/* globals define*/ + +define(function() { + var TopicSelect = {}; + + TopicSelect.init = function(onSelect) { + $('#topics-container').on('click', '.select', function() { + var select = $(this); + var isChecked = !select.hasClass('fa-square-o'); + + select.toggleClass('fa-check-square-o', !isChecked); + select.toggleClass('fa-square-o', isChecked); + select.parents('.category-item').toggleClass('selected', !isChecked); + if (typeof onSelect === 'function') { + onSelect(); + } + }); + }; + + TopicSelect.getSelectedTids = function() { + var tids = []; + $('#topics-container .category-item.selected').each(function() { + tids.push($(this).attr('data-tid')); + }); + return tids; + }; + + TopicSelect.unselectAll = function() { + $('#topics-container .category-item.selected').removeClass('selected'); + $('#topics-container .select').toggleClass('fa-check-square-o', false).toggleClass('fa-square-o', true); + }; + + return TopicSelect; +}); \ No newline at end of file diff --git a/src/categories/activeusers.js b/src/categories/activeusers.js index 418b891aa9..9b8e558aae 100644 --- a/src/categories/activeusers.js +++ b/src/categories/activeusers.js @@ -57,7 +57,7 @@ module.exports = function(Categories) { db.getSortedSetRevRange('cid:' + cid + ':active_users', 0, 23, callback); }; - Categories.moveActiveUsers = function(tid, oldCid, cid, callback) { + Categories.moveActiveUsers = function(tid, oldCid, cid) { function updateUser(uid, timestamp) { Categories.addActiveUser(cid, uid, timestamp); Categories.isUserActiveIn(oldCid, uid, function(err, active) { diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js index 8f4b571e9e..33ee629811 100644 --- a/src/categories/recentreplies.js +++ b/src/categories/recentreplies.js @@ -22,25 +22,27 @@ module.exports = function(Categories) { }); }; - Categories.moveRecentReplies = function(tid, oldCid, cid, callback) { - function movePost(pid, callback) { + Categories.moveRecentReplies = function(tid, oldCid, cid) { + function movePost(pid, next) { posts.getPostField(pid, 'timestamp', function(err, timestamp) { if(err) { - return callback(err); + return next(err); } db.sortedSetRemove('categories:recent_posts:cid:' + oldCid, pid); db.sortedSetAdd('categories:recent_posts:cid:' + cid, timestamp, pid); - callback(); + next(); }); } topics.getPids(tid, function(err, pids) { - if(err) { - return callback(err); + if(!err && pids) { + async.each(pids, movePost, function(err) { + if (err) { + winston.error(err.message); + } + }); } - - async.each(pids, movePost, callback); }); }; }; diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index 17e2ef3684..4d183a1fb3 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -120,64 +120,72 @@ SocketTopics.markCategoryTopicsRead = function(socket, cid, callback) { }); }; -SocketTopics.markAsUnreadForAll = function(socket, tid, callback) { - topics.markAsUnreadForAll(tid, function(err) { - if(err) { - return callback(err); - } - - db.sortedSetAdd('topics:recent', Date.now(), tid, function(err) { - if(err) { - return callback(err); - } - topics.pushUnreadCount(); - callback(); - }); - }); -}; - -SocketTopics.delete = function(socket, tid, callback) { - doTopicAction('delete', socket, tid, callback); -}; - -SocketTopics.restore = function(socket, tid, callback) { - doTopicAction('restore', socket, tid, callback); -}; - -SocketTopics.lock = function(socket, tid, callback) { - doTopicAction('lock', socket, tid, callback); -}; - -SocketTopics.unlock = function(socket, tid, callback) { - doTopicAction('unlock', socket, tid, callback); -}; - -SocketTopics.pin = function(socket, tid, callback) { - doTopicAction('pin', socket, tid, callback); -}; - -SocketTopics.unpin = function(socket, tid, callback) { - doTopicAction('unpin', socket, tid, callback); -}; - -function doTopicAction(action, socket, tid, callback) { - if(!tid) { +SocketTopics.markAsUnreadForAll = function(socket, tids, callback) { + if(!Array.isArray(tids)) { return callback(new Error('[[error:invalid-tid]]')); } - threadTools.privileges(tid, socket.uid, function(err, privileges) { - if(err) { - return callback(err); - } + async.each(tids, function(tid, next) { + topics.markAsUnreadForAll(tid, function(err) { + if(err) { + return next(err); + } - if(!privileges || !privileges.editable) { - return callback(new Error('[[error:no-privileges]]')); - } + db.sortedSetAdd('topics:recent', Date.now(), tid, function(err) { + if(err) { + return next(err); + } + topics.pushUnreadCount(); + next(); + }); + }); + }, callback); +}; - if(threadTools[action]) { - threadTools[action](tid, socket.uid, callback); - } - }); +SocketTopics.delete = function(socket, tids, callback) { + doTopicAction('delete', socket, tids, callback); +}; + +SocketTopics.restore = function(socket, tids, callback) { + doTopicAction('restore', socket, tids, callback); +}; + +SocketTopics.lock = function(socket, tids, callback) { + doTopicAction('lock', socket, tids, callback); +}; + +SocketTopics.unlock = function(socket, tids, callback) { + doTopicAction('unlock', socket, tids, callback); +}; + +SocketTopics.pin = function(socket, tids, callback) { + doTopicAction('pin', socket, tids, callback); +}; + +SocketTopics.unpin = function(socket, tids, callback) { + doTopicAction('unpin', socket, tids, callback); +}; + +function doTopicAction(action, socket, tids, callback) { + if(!tids) { + return callback(new Error('[[error:invalid-tid]]')); + } + + async.each(tids, function(tid, next) { + threadTools.privileges(tid, socket.uid, function(err, privileges) { + if(err) { + return next(err); + } + + if(!privileges || !privileges.editable) { + return next(new Error('[[error:no-privileges]]')); + } + + if(typeof threadTools[action] === 'function') { + threadTools[action](tid, socket.uid, next); + } + }); + }, callback); } SocketTopics.createTopicFromPosts = function(socket, data, callback) { @@ -215,21 +223,45 @@ SocketTopics.movePost = function(socket, data, callback) { }; SocketTopics.move = function(socket, data, callback) { - if(!data || !data.tid || !data.cid) { + if(!data || !Array.isArray(data.tids) || !data.cid) { return callback(new Error('[[error:invalid-data]]')); } - threadTools.move(data.tid, data.cid, function(err) { - if(err) { - return callback(err); - } + async.each(data.tids, function(tid, next) { + var oldCid; + async.waterfall([ + function(next) { + threadTools.privileges(tid, socket.uid, next); + }, + function(privileges, next) { + if(!(privileges.admin || privileges.moderator)) { + return next(new Error('[[error:no-privileges]]')); + } + next(); + }, + function(next) { + topics.getTopicField(tid, 'cid', next); + }, + function(cid, next) { + oldCid = cid; + threadTools.move(tid, data.cid, next); + } + ], function(err) { + if(err) { + return next(err); + } - index.server.sockets.in('topic_' + data.tid).emit('event:topic_moved', { - tid: data.tid + index.server.sockets.in('topic_' + tid).emit('event:topic_moved', { + tid: tid + }); + + index.server.sockets.in('category_' + oldCid).emit('event:topic_moved', { + tid: tid + }); + + next(); }); - - callback(null); - }); + }, callback); }; SocketTopics.followCheck = function(socket, tid, callback) { diff --git a/src/threadTools.js b/src/threadTools.js index d2de95bb3f..55bac851b4 100644 --- a/src/threadTools.js +++ b/src/threadTools.js @@ -63,15 +63,14 @@ var winston = require('winston'), }; function toggleDelete(tid, uid, isDelete, callback) { - topics.getTopicField(tid, 'deleted', function(err, deleted) { + topics.getTopicFields(tid, ['cid', 'deleted'], function(err, topicData) { if(err) { return callback(err); } - if (parseInt(deleted, 10) && isDelete) { - return callback(new Error('[[error:topic-already-deleted]]')); - } else if (!parseInt(deleted, 10) && !isDelete) { - return callback(new Error('[[error:topic-already-restored]]')); + var alreadyDeletedOrRestored = (parseInt(topicData.deleted, 10) && isDelete) || (!parseInt(topicData.deleted, 10) && !isDelete); + if (alreadyDeletedOrRestored) { + return callback(null, {tid: tid}); } topics[isDelete ? 'delete' : 'restore'](tid, function(err) { @@ -88,7 +87,13 @@ var winston = require('winston'), websockets.emitTopicPostStats(); websockets.in('topic_' + tid).emit(isDelete ? 'event:topic_deleted' : 'event:topic_restored', { - tid: tid + tid: tid, + isDelete: isDelete + }); + + websockets.in('category_' + topicData.cid).emit(isDelete ? 'event:topic_deleted' : 'event:topic_restored', { + tid: tid, + isDelete: isDelete }); callback(null, { @@ -107,17 +112,30 @@ var winston = require('winston'), }; function toggleLock(tid, uid, lock, callback) { - topics.setTopicField(tid, 'locked', lock ? 1 : 0); + topics.getTopicField(tid, 'cid', function(err, cid) { + if (err) { + return callback(err); + } - websockets.in('topic_' + tid).emit(lock ? 'event:topic_locked' : 'event:topic_unlocked', { - tid: tid - }); + topics.setTopicField(tid, 'locked', lock ? 1 : 0); - if (typeof callback === 'function') { - callback(null, { - tid: tid + websockets.in('topic_' + tid).emit(lock ? 'event:topic_locked' : 'event:topic_unlocked', { + tid: tid, + isLocked: lock }); - } + + websockets.in('category_' + cid).emit(lock ? 'event:topic_locked' : 'event:topic_unlocked', { + tid: tid, + isLocked: lock + }); + + if (typeof callback === 'function') { + callback(null, { + tid: tid, + isLocked: lock + }); + } + }); } ThreadTools.pin = function(tid, uid, callback) { @@ -129,20 +147,33 @@ var winston = require('winston'), }; function togglePin(tid, uid, pin, callback) { - topics.setTopicField(tid, 'pinned', pin ? 1 : 0); - topics.getTopicFields(tid, ['cid', 'lastposttime'], function(err, topicData) { - db.sortedSetAdd('categories:' + topicData.cid + ':tid', pin ? Math.pow(2, 53) : topicData.lastposttime, tid); - }); + topics.getTopicField(tid, 'cid', function(err, cid) { + if (err) { + return callback(err); + } - websockets.in('topic_' + tid).emit(pin ? 'event:topic_pinned' : 'event:topic_unpinned', { - tid: tid - }); - - if (typeof callback === 'function') { - callback(null, { - tid: tid + topics.setTopicField(tid, 'pinned', pin ? 1 : 0); + topics.getTopicFields(tid, ['cid', 'lastposttime'], function(err, topicData) { + db.sortedSetAdd('categories:' + topicData.cid + ':tid', pin ? Math.pow(2, 53) : topicData.lastposttime, tid); }); - } + + websockets.in('topic_' + tid).emit(pin ? 'event:topic_pinned' : 'event:topic_unpinned', { + tid: tid, + isPinned: pin + }); + + websockets.in('category_' + cid).emit(pin ? 'event:topic_pinned' : 'event:topic_unpinned', { + tid: tid, + isPinned: pin + }); + + if (typeof callback === 'function') { + callback(null, { + tid: tid, + isPinned: pin + }); + } + }); } ThreadTools.move = function(tid, cid, callback) { @@ -165,8 +196,6 @@ var winston = require('winston'), } var oldCid = topic.cid; - topics.setTopicField(tid, 'cid', cid); - if(!parseInt(topic.deleted, 10)) { categories.incrementCategoryFieldBy(oldCid, 'topic_count', -1); categories.incrementCategoryFieldBy(cid, 'topic_count', 1); @@ -174,7 +203,9 @@ var winston = require('winston'), categories.moveActiveUsers(tid, oldCid, cid); - categories.moveRecentReplies(tid, oldCid, cid, callback); + categories.moveRecentReplies(tid, oldCid, cid); + + topics.setTopicField(tid, 'cid', cid, callback); }); };