diff --git a/config.default.js b/config.default.js index fb64d54dcf..562c5a194d 100644 --- a/config.default.js +++ b/config.default.js @@ -36,6 +36,11 @@ var config = { "facebook": { "app_id": '', "secret": '' + }, + + // Privileged Actions Reputation Thresholds + "privilege_thresholds": { + "manage_thread": 1000 } } diff --git a/public/css/style.less b/public/css/style.less index 0c2c357186..0dccfdfdf0 100644 --- a/public/css/style.less +++ b/public/css/style.less @@ -204,6 +204,10 @@ footer.footer { background: #fff; } + &.deleted { + -moz-opacity: 0.30; + opacity: 0.30; + } } #user_label { diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index f03e3921de..716c26d977 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -41,7 +41,6 @@ var ajaxify = {}; jQuery('#content').fadeOut(100); load_template(function() { - console.log('called'); exec_body_scripts(content); ajaxify.enable(); diff --git a/public/templates/category.tpl b/public/templates/category.tpl index 513070c198..21516dfed2 100644 --- a/public/templates/category.tpl +++ b/public/templates/category.tpl @@ -2,7 +2,7 @@ <ul class="topic-container"> <!-- BEGIN topics --> <a href="../../topic/{topics.slug}"><li class="topic-row"> - <h4>{topics.title}</h4> + <h4><i class="{topics.icon}"></i> {topics.title}</h4> <p>Posted {topics.relativeTime} ago by <span class="username">{topics.username}</span>. {topics.post_count} posts.</p> </li></a> <!-- END topics --> diff --git a/public/templates/topic.tpl b/public/templates/topic.tpl index 25c48d99ac..2166b2ffe5 100644 --- a/public/templates/topic.tpl +++ b/public/templates/topic.tpl @@ -34,31 +34,72 @@ </ul> <hr /> <button id="post_reply" class="btn btn-primary btn-large post_reply">Reply</button> -<div class="btn-group pull-right"> +<div class="btn-group pull-right" id="thread-tools" style="visibility: hidden;"> <button class="btn dropdown-toggle" data-toggle="dropdown">Thread Tools <span class="caret"></span></button> <ul class="dropdown-menu"> - <li><a href="#">Lock/Unlock Thread</a></li> + <li><a href="#" id="lock_thread"><i class="icon-lock"></i> Lock Thread</a></li> <li class="divider"></li> - <li><a href="#"><span class="text-error">Delete Thread</span></a></li> + <li><a href="#" id="delete_thread"><span class="text-error"><i class="icon-trash"></i> Delete Thread</span></a></li> </ul> </div> <script type="text/javascript"> (function() { - var locked = '{locked}'; + var expose_tools = '{expose_tools}', + tid = '{topic_id}', + thread_state = { + locked: '{locked}', + deleted: '{deleted}' + }; jQuery('document').ready(function() { - var room = 'topic_' + '{topic_id}'; + var room = 'topic_' + '{topic_id}', + adminTools = document.getElementById('thread-tools'); app.enter_room(room); set_up_posts(); - if (locked === '1') set_locked_state(true); + if (thread_state.locked === '1') set_locked_state(true); + if (thread_state.deleted === '1') set_delete_state(true); + + if (expose_tools === '1') { + var deleteThreadEl = document.getElementById('delete_thread'), + lockThreadEl = document.getElementById('lock_thread'); + + adminTools.style.visibility = 'inherit'; + + // Add events to the thread tools + deleteThreadEl.addEventListener('click', function(e) { + e.preventDefault(); + if (thread_state.deleted !== '1') { + if (confirm('really delete thread? (THIS DIALOG TO BE REPLACED WITH BOOTBOX)')) { + socket.emit('api:topic.delete', { tid: tid }); + } + } else { + if (confirm('really restore thread? (THIS DIALOG TO BE REPLACED WITH BOOTBOX)')) { + socket.emit('api:topic.restore', { tid: tid }); + } + } + }); + + lockThreadEl.addEventListener('click', function(e) { + e.preventDefault(); + if (thread_state.locked !== '1') { + if (confirm('really lock thread? (THIS DIALOG TO BE REPLACED WITH BOOTBOX)')) { + socket.emit('api:topic.lock', { tid: tid }); + } + } else { + if (confirm('really unlock thread? (THIS DIALOG TO BE REPLACED WITH BOOTBOX)')) { + socket.emit('api:topic.unlock', { tid: tid }); + } + } + }); + } }); - ajaxify.register_events(['event:rep_up', 'event:rep_down', 'event:new_post', 'api:get_users_in_room']); + ajaxify.register_events(['event:rep_up', 'event:rep_down', 'event:new_post', 'api:get_users_in_room', 'event:topic_deleted']); socket.on('api:get_users_in_room', function(users) { var anonymous = users.anonymous, usernames = users.usernames, @@ -88,7 +129,6 @@ adjust_rep(-1, data.pid, data.uid); }); - socket.on('event:new_post', function(data) { var html = templates.prepare(templates['topic'].blocks['posts']).parse(data), uniqueid = new Date().getTime(); @@ -97,7 +137,31 @@ set_up_posts(uniqueid); }); + socket.on('event:topic_deleted', function(data) { + if (data.tid === tid && data.status === 'ok') { + set_locked_state(true); + set_delete_state(true); + } + }); + + socket.on('event:topic_restored', function(data) { + if (data.tid === tid && data.status === 'ok') { + set_locked_state(false); + set_delete_state(false); + } + }); + + socket.on('event:topic_locked', function(data) { + if (data.tid === tid && data.status === 'ok') { + set_locked_state(true); + } + }); + socket.on('event:topic_unlocked', function(data) { + if (data.tid === tid && data.status === 'ok') { + set_locked_state(false); + } + }); function adjust_rep(value, pid, uid) { var post_rep = jQuery('.post_rep_' + pid), @@ -119,11 +183,11 @@ else div = '#' + div; jQuery(div + ' .post_reply').click(function() { - if (locked !== '1') app.open_post_window('reply', "{topic_id}", "{topic_name}"); + if (thread_state.locked !== '1') app.open_post_window('reply', "{topic_id}", "{topic_name}"); }); jQuery(div + ' .quote').click(function() { - if (locked !== '1') app.open_post_window('quote', "{topic_id}", "{topic_name}"); + if (thread_state.locked !== '1') app.open_post_window('quote', "{topic_id}", "{topic_name}"); // this needs to be looked at, obviously. only single line quotes work well I think maybe replace all \r\n with > ? document.getElementById('post_content').innerHTML = '> ' + document.getElementById('content_' + this.id.replace('quote_', '')).innerHTML; @@ -142,13 +206,15 @@ uid = ids[1]; - if (this.children[1].className == 'icon-star-empty') { - this.children[1].className = 'icon-star'; - socket.emit('api:posts.favourite', {pid: pid, room_id: app.current_room}); - } - else { - this.children[1].className = 'icon-star-empty'; - socket.emit('api:posts.unfavourite', {pid: pid, room_id: app.current_room}); + if (thread_state.locked !== '1') { + if (this.children[1].className == 'icon-star-empty') { + this.children[1].className = 'icon-star'; + socket.emit('api:posts.favourite', {pid: pid, room_id: app.current_room}); + } + else { + this.children[1].className = 'icon-star-empty'; + socket.emit('api:posts.unfavourite', {pid: pid, room_id: app.current_room}); + } } }); } @@ -158,21 +224,54 @@ postReplyBtns = document.querySelectorAll('#post-container .post_reply'), quoteBtns = document.querySelectorAll('#post-container .quote'), numReplyBtns = postReplyBtns.length, + lockThreadEl = document.getElementById('lock_thread'), x; if (locked === true) { + lockThreadEl.innerHTML = '<i class="icon-unlock"></i> Unlock Thread'; threadReplyBtn.disabled = true; threadReplyBtn.innerHTML = 'Locked <i class="icon-lock"></i>'; for(x=0;x<numReplyBtns;x++) { postReplyBtns[x].innerHTML = 'Locked <i class="icon-lock"></i>'; quoteBtns[x].style.display = 'none'; } + + thread_state.locked = '1'; } else { + lockThreadEl.innerHTML = '<i class="icon-lock"></i> Lock Thread'; threadReplyBtn.disabled = false; threadReplyBtn.innerHTML = 'Reply'; for(x=0;x<numReplyBtns;x++) { postReplyBtns[x].innerHTML = 'Reply <i class="icon-reply"></i>'; quoteBtns[x].style.display = 'inline-block'; } + + thread_state.locked = '0'; + } + } + + function set_delete_state(deleted) { + var deleteThreadEl = document.getElementById('delete_thread'), + deleteTextEl = deleteThreadEl.getElementsByTagName('span')[0], + threadEl = document.querySelector('.post-container'), + deleteNotice = document.getElementById('thread-deleted') || document.createElement('div'); + + if (deleted) { + deleteTextEl.innerHTML = '<i class="icon-comment"></i> Restore Thread'; + $(threadEl).addClass('deleted'); + + // Spawn a 'deleted' notice at the top of the page + deleteNotice.setAttribute('id', 'thread-deleted'); + deleteNotice.className = 'alert'; + deleteNotice.innerHTML = 'This thread has been deleted. Only users with thread management privileges can see it.<br /><br /><a href="/">Home</a>'; + document.getElementById('content').insertBefore(deleteNotice, threadEl); + + thread_state.deleted = '1'; + } else { + deleteTextEl.innerHTML = '<i class="icon-trash"></i> Delete Thread'; + $(threadEl).removeClass('deleted'); + deleteNotice.parentNode.removeChild(deleteNotice); + + thread_state.deleted = '0'; } } })(); diff --git a/src/posts.js b/src/posts.js index 4c729361b1..6683ed6307 100644 --- a/src/posts.js +++ b/src/posts.js @@ -1,7 +1,8 @@ var RDB = require('./redis.js'), utils = require('./utils.js'), marked = require('marked'), - user = require('./user.js'); + user = require('./user.js'), + config = require('../config.js'); (function(Posts) { @@ -9,12 +10,12 @@ var RDB = require('./redis.js'), if (start == null) start = 0; if (end == null) end = start + 10; - var post_data, user_data, thread_data, vote_data; + var post_data, user_data, thread_data, vote_data, viewer_data; //compile thread after all data is asynchronously called function generateThread() { - if (!post_data ||! user_data || !thread_data || !vote_data) return; + if (!post_data ||! user_data || !thread_data || !vote_data || !viewer_data) return; var posts = []; @@ -33,14 +34,16 @@ var RDB = require('./redis.js'), 'user_rep' : user_data[uid].reputation || 0, 'gravatar' : user_data[uid].picture, 'fav_star_class' : vote_data[pid] ? 'icon-star' : 'icon-star-empty', - 'display_moderator_tools' : uid === current_user ? 'show' : 'hide' + 'display_moderator_tools' : uid == current_user ? 'show' : 'hide' }); } callback({ 'topic_name':thread_data.topic_name, 'locked': parseInt(thread_data.locked) || 0, + 'deleted': parseInt(thread_data.deleted) || 0, 'topic_id': tid, + 'expose_tools': viewer_data.reputation >= config.privilege_thresholds.manage_thread ? 1 : 0, 'posts': posts }); } @@ -76,6 +79,7 @@ var RDB = require('./redis.js'), .mget(post_rep) .get('tid:' + tid + ':title') .get('tid:' + tid + ':locked') + .get('tid:' + tid + ':deleted') .exec(function(err, replies) { post_data = { pid: pids, @@ -87,7 +91,8 @@ var RDB = require('./redis.js'), thread_data = { topic_name: replies[4], - locked: replies[5] + locked: replies[5] || 0, + deleted: replies[6] || 0 }; user.getMultipleUserFields(post_data.uid, ['username','reputation','picture'], function(user_details){ @@ -95,7 +100,13 @@ var RDB = require('./redis.js'), generateThread(); }); }); - + }); + + user.getUserField(current_user, 'reputation', function(reputation){ + viewer_data = { + reputation: reputation + }; + generateThread(); }); } @@ -116,8 +127,8 @@ var RDB = require('./redis.js'), user.getUserFields(uid, ['username','reputation','picture'], function(data){ var timestamp = new Date().getTime(); - - socket.in('topic_' + tid).emit('event:new_post', { + + io.sockets.in('topic_' + tid).emit('event:new_post', { 'posts' : [ { 'pid' : pid, diff --git a/src/topics.js b/src/topics.js index be6bf06e32..a71103efcc 100644 --- a/src/topics.js +++ b/src/topics.js @@ -1,7 +1,8 @@ var RDB = require('./redis.js'), posts = require('./posts.js'), utils = require('./utils.js'), - user = require('./user.js'); + user = require('./user.js'), + configs = require('../config.js'); (function(Topics) { @@ -23,14 +24,18 @@ var RDB = require('./redis.js'), uid = [], timestamp = [], slug = [], - postcount = []; + postcount = [], + locked = [], + deleted = []; for (var i=0, ii=tids.length; i<ii; i++) { title.push('tid:' + tids[i] + ':title'); uid.push('tid:' + tids[i] + ':uid'); timestamp.push('tid:' + tids[i] + ':timestamp'); slug.push('tid:' + tids[i] + ':slug'); - postcount.push('tid:' + tids[i] + ':postcount'); + postcount.push('tid:' + tids[i] + ':postcount'), + locked.push('tid:' + tids[i] + ':locked'), + deleted.push('tid:' + tids[i] + ':deleted'); } if (tids.length > 0) { @@ -40,21 +45,23 @@ var RDB = require('./redis.js'), .mget(timestamp) .mget(slug) .mget(postcount) + .mget(locked) + .mget(deleted) .exec(function(err, replies) { - title = replies[0]; uid = replies[1]; timestamp = replies[2]; slug = replies[3]; postcount = replies[4]; - - - + locked = replies[5]; + deleted = replies[6]; + user.get_usernames_by_uids(uid, function(userNames) { var topics = []; for (var i=0, ii=title.length; i<ii; i++) { - + if (deleted[i] === '1') continue; + topics.push({ 'title' : title[i], 'uid' : uid[i], @@ -62,7 +69,9 @@ var RDB = require('./redis.js'), 'timestamp' : timestamp[i], 'relativeTime': utils.relativeTime(timestamp[i]), 'slug' : slug[i], - 'post_count' : postcount[i] + 'post_count' : postcount[i], + icon: locked[i] === '1' ? 'icon-lock' : 'hide', + deleted: deleted[i] }); } @@ -139,8 +148,71 @@ var RDB = require('./redis.js'), timeout: 2000 }); }); - - }; + Topics.lock = function(tid, uid, socket) { + user.getUserField(uid, 'reputation', function(rep) { + if (rep >= configs.privilege_thresholds.manage_thread) { + // Mark thread as locked + RDB.set('tid:' + tid + ':locked', 1); + + if (socket) { + io.sockets.in('topic_' + tid).emit('event:topic_locked', { + tid: tid, + status: 'ok' + }); + } + } + }); + } + + Topics.unlock = function(tid, uid, socket) { + user.getUserField(uid, 'reputation', function(rep) { + if (rep >= configs.privilege_thresholds.manage_thread) { + // Mark thread as locked + RDB.del('tid:' + tid + ':locked'); + + if (socket) { + io.sockets.in('topic_' + tid).emit('event:topic_unlocked', { + tid: tid, + status: 'ok' + }); + } + } + }); + } + + Topics.delete = function(tid, uid, socket) { + user.getUserField(uid, 'reputation', function(rep) { + if (rep >= configs.privilege_thresholds.manage_thread) { + // Mark thread as deleted + RDB.set('tid:' + tid + ':deleted', 1); + Topics.lock(tid, uid); + + if (socket) { + io.sockets.in('topic_' + tid).emit('event:topic_deleted', { + tid: tid, + status: 'ok' + }); + } + } + }); + } + + Topics.restore = function(tid, uid, socket) { + user.getUserField(uid, 'reputation', function(rep) { + if (rep >= configs.privilege_thresholds.manage_thread) { + // Mark thread as deleted + RDB.del('tid:' + tid + ':deleted'); + Topics.unlock(tid, uid); + + if (socket) { + io.sockets.in('topic_' + tid).emit('event:topic_restored', { + tid: tid, + status: 'ok' + }); + } + } + }); + } }(exports)); \ No newline at end of file diff --git a/src/webserver.js b/src/webserver.js index 66af552512..e23c3ddc00 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -411,9 +411,8 @@ passport.deserializeUser(function(uid, done) { app.get('/test', function(req, res) { - global.modules.posts.get(function(data) { - res.send('<pre>' + JSON.stringify(data, null, 4) + '</pre>'); - }, 1, 1); + global.modules.topics.delete(1, 1); + res.send(); }); }(WebServer)); diff --git a/src/websockets.js b/src/websockets.js index 9ce21f4d41..b8ddb7b39c 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -62,10 +62,7 @@ var SocketIO = require('socket.io').listen(global.server,{log:false}), socket.emit('event:connect', {status: 1}); socket.on('disconnect', function() { - console.log('Got disconnect! SESSION ID : '+hs.sessionID+' USER ID : '+uid); - delete users[hs.sessionID]; - console.log(users); }); @@ -177,6 +174,22 @@ var SocketIO = require('socket.io').listen(global.server,{log:false}), socket.on('api:user.active.get_record', function() { modules.user.active.get_record(socket); }); + + socket.on('api:topic.delete', function(data) { + modules.topics.delete(data.tid, uid, socket); + }); + + socket.on('api:topic.restore', function(data) { + modules.topics.restore(data.tid, uid, socket); + }); + + socket.on('api:topic.lock', function(data) { + modules.topics.lock(data.tid, uid, socket); + }); + + socket.on('api:topic.unlock', function(data) { + modules.topics.unlock(data.tid, uid, socket); + }); }); }(SocketIO));