diff --git a/mocks/databasemock.js b/mocks/databasemock.js index a75a1e89a1..3b99f075ff 100644 --- a/mocks/databasemock.js +++ b/mocks/databasemock.js @@ -79,7 +79,7 @@ } winston.info('test_database flushed'); - /* + meta.configs.init(function () { nconf.set('url', nconf.get('base_url') + (nconf.get('use_port') ? ':' + nconf.get('port') : '') + nconf.get('relative_path')); nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-vanilla/templates')); @@ -90,8 +90,7 @@ sockets.init(webserver.server); done(); - });*/ - done(); + }); }); }); }); diff --git a/package.json b/package.json index 25eed44af8..42015d4afe 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "start": "./nodebb start", "stop": "./nodebb stop", - "test": "mocha ./tests/database.js -t 1000" + "test": "mocha ./tests -t 10000" }, "dependencies": { "express": "3.2.0", diff --git a/public/language/en_GB/global.json b/public/language/en_GB/global.json index c935f1c5d8..de15a7e4ce 100644 --- a/public/language/en_GB/global.json +++ b/public/language/en_GB/global.json @@ -69,6 +69,6 @@ "invisible": "Invisible", "offline": "Offline", - "privacy": "Privacy", + "email": "Email", "language": "Language" } diff --git a/public/language/en_GB/notifications.json b/public/language/en_GB/notifications.json index 748c89bfb7..d480d39a2e 100644 --- a/public/language/en_GB/notifications.json +++ b/public/language/en_GB/notifications.json @@ -9,5 +9,13 @@ "continue_to": "Continue to", "return_to": "Return to ", "new_notification": "New Notification", - "you_have_unread_notifications": "You have unread notifications." + "you_have_unread_notifications": "You have unread notifications.", + + "user_made_post": "%1 made a new post", + "new_message_from": "New message from %1", + "upvoted_your_post": "%1 has upvoted your post.", + "favourited_your_post": "%1 has favourited your post.", + "user_flagged_post": "%1 flagged a post.", + "user_posted_to" : "%1 has posted a reply to: %2" + } diff --git a/public/language/en_GB/user.json b/public/language/en_GB/user.json index 1ca6651efb..a9a384da43 100644 --- a/public/language/en_GB/user.json +++ b/public/language/en_GB/user.json @@ -47,6 +47,12 @@ "settings": "Settings", "show_email": "Show My Email", + "digest_label": "Subscribe to Digest", + "digest_description": "Subscribe to email updates for this forum (new notifications and topics) according to a set schedule", + "digest_off": "Off", + "digest_daily": "Daily", + "digest_weekly": "Weekly", + "digest_monthly": "Monthly", "has_no_follower": "This user doesn't have any followers :(", "follows_no_one": "This user isn't following anyone :(", diff --git a/public/src/forum/accountsettings.js b/public/src/forum/accountsettings.js index bb18cacb2c..4945eb8abb 100644 --- a/public/src/forum/accountsettings.js +++ b/public/src/forum/accountsettings.js @@ -16,11 +16,11 @@ define(['forum/accountheader'], function(header) { } switch (input.attr('type')) { - case 'text' : - case 'textarea' : + case 'text': + case 'textarea': settings[setting] = input.val(); break; - case 'checkbox' : + case 'checkbox': settings[setting] = input.is(':checked') ? 1 : 0; break; } diff --git a/public/src/forum/admin/plugins.js b/public/src/forum/admin/plugins.js index a494f3053d..01b4e4589f 100644 --- a/public/src/forum/admin/plugins.js +++ b/public/src/forum/admin/plugins.js @@ -1,3 +1,6 @@ +"use strict"; +/* global define, app, socket */ + define(function() { var Plugins = { init: function() { @@ -19,10 +22,13 @@ define(function() { app.alert({ alert_id: 'plugin_toggled', title: 'Plugin ' + (status.active ? 'Enabled' : 'Disabled'), - message: 'Restarting your NodeBB ', - type: 'warning', - timeout: 5000 - }) + message: 'Please restart your NodeBB to fully ' + (status.active ? 'activate' : 'deactivate') + ' this plugin', + type: 'info', + timeout: 5000, + clickfn: function() { + socket.emit('admin.restart'); + } + }); }); } else { pluginsList.append('
  • No plugins found.

  • '); diff --git a/public/src/forum/admin/themes.js b/public/src/forum/admin/themes.js index 4dd38c1d92..e74887d8f2 100644 --- a/public/src/forum/admin/themes.js +++ b/public/src/forum/admin/themes.js @@ -39,10 +39,13 @@ define(['forum/admin/settings'], function(Settings) { app.alert({ alert_id: 'admin:theme', - type: 'success', + type: 'info', title: 'Theme Changed', - message: 'Restarting your NodeBB ', - timeout: 3500 + message: 'Please restart your NodeBB to fully activate this theme', + timeout: 5000, + clickfn: function() { + socket.emit('admin.restart'); + } }); }); } @@ -251,7 +254,7 @@ define(['forum/admin/settings'], function(Settings) { timeout: 2500 }); } - + }); }); } diff --git a/public/src/forum/category.js b/public/src/forum/category.js index adb5bc34c7..8acd9543ce 100644 --- a/public/src/forum/category.js +++ b/public/src/forum/category.js @@ -225,11 +225,13 @@ define(['composer', 'forum/pagination', 'share', 'navigator'], function(composer }); } - Category.onTopicsLoaded = function(topics, callback) { - if(!topics || !topics.length) { + Category.onTopicsLoaded = function(data, callback) { + if(!data || !data.topics.length) { return; } + var topics = data.topics; + function removeAlreadyAddedTopics() { topics = topics.filter(function(topic) { return $('#topics-container li[data-tid="' + topic.tid +'"]').length === 0; @@ -261,7 +263,7 @@ define(['composer', 'forum/pagination', 'share', 'navigator'], function(composer findInsertionPoint(); ajaxify.loadTemplate('category', function(categoryTemplate) { - var html = templates.parse(templates.getBlock(categoryTemplate, 'topics'), {topics: topics}); + var html = templates.parse(templates.getBlock(categoryTemplate, 'topics'), data); translator.translate(html, function(translatedHTML) { var container = $('#topics-container'), @@ -309,6 +311,7 @@ define(['composer', 'forum/pagination', 'share', 'navigator'], function(composer cid: cid, after: after }, function (err, data) { + console.log(data); loadingMoreTopics = false; if(err) { @@ -316,7 +319,7 @@ define(['composer', 'forum/pagination', 'share', 'navigator'], function(composer } if (data && data.topics.length) { - Category.onTopicsLoaded(data.topics, callback); + Category.onTopicsLoaded(data, callback); $('#topics-container').attr('data-nextstart', data.nextStart); } else { diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index 9987a5cedd..2d4120b903 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -1,3 +1,8 @@ +'use strict'; + +/* globals define, socket, translator, utils, config, app, ajaxify, Tinycon*/ + + define(['sounds'], function(sound) { var Notifications = {}; @@ -13,50 +18,44 @@ define(['sounds'], function(sound) { socket.emit('notifications.get', null, function(err, data) { - var numRead = data.read.length, - numUnread = data.unread.length, - x; + function createNotification(notification, callback) { + if (notification.image) { + image = ''; + } else { + image = ''; + } + + return '
  • ' + image + '' + utils.relativeTime(notification.datetime, true) + '' + notification.text + '
  • '; + } - var html = ''; + var x, html = ''; if (!err && (data.read.length + data.unread.length) > 0) { var image = ''; - for (x = 0; x < numUnread; x++) { - if (data.unread[x].image) { - image = ''; - } else { - image = ''; - } - html += '
  • ' + image + '' + utils.relativeTime(data.unread[x].datetime, true) + '' + data.unread[x].text + '
  • '; + for (x = 0; x < data.unread.length; x++) { + html += createNotification(data.unread[x]); } - for (x = 0; x < numRead; x++) { - if (data.read[x].image) { - image = ''; - } else { - image = ''; - } - html += '
  • ' + image + '' + utils.relativeTime(data.read[x].datetime, true) + '' + data.read[x].text + '
  • '; + for (x = 0; x < data.read.length; x++) { + html += createNotification(data.read[x]); } - addSeeAllLink(replaceHtml); + + addSeeAllLink(); } else { - translator.translate('
  • [[notifications:no_notifs]]
  • ', function(translated) { - html += translated; - addSeeAllLink(replaceHtml); - }); + html += '
  • [[notifications:no_notifs]]
  • '; + addSeeAllLink(); } - function addSeeAllLink(callback) { - translator.translate('', function(translated) { - html += translated; - callback(); - }); + function addSeeAllLink() { + html += ''; } - function replaceHtml() { - notifList.html(html); - } + + translator.translate(html, function(translated) { + notifList.html(translated); + }); + updateNotifCount(data.unread.length); diff --git a/src/categoryTools.js b/src/categoryTools.js index d07f5007de..cfbe00d53f 100644 --- a/src/categoryTools.js +++ b/src/categoryTools.js @@ -2,6 +2,7 @@ var Groups = require('./groups'), User = require('./user'), + categories = require('./categories'), async = require('async'), db = require('./database'); @@ -36,6 +37,9 @@ CategoryTools.exists = function(cid, callback) { CategoryTools.privileges = function(cid, uid, callback) { async.parallel({ + "disabled": function(next) { + categories.getCategoryField(cid, 'disabled', next); + }, "+r": function(next) { internals.isMember('cid:' + cid + ':privileges:+r', uid, next); }, @@ -62,6 +66,7 @@ CategoryTools.privileges = function(cid, uid, callback) { "g+w": privileges['g+w'], read: ( ( + parseInt(privileges.disabled, 10) !== 1 && (privileges['+r'] || privileges['+r'] === null) && (privileges['g+r'] || privileges['g+r'] === null) ) || @@ -70,6 +75,7 @@ CategoryTools.privileges = function(cid, uid, callback) { ), write: ( ( + parseInt(privileges.disabled, 10) !== 1 && (privileges['+w'] || privileges['+w'] === null) && (privileges['g+w'] || privileges['g+w'] === null) ) || diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js index 97678b4963..0eab10a25f 100644 --- a/src/controllers/accounts.js +++ b/src/controllers/accounts.js @@ -348,9 +348,15 @@ accountsController.accountSettings = function(req, res, next) { return userNotFound(); } - results.user.yourid = req.user.uid; - results.user.theirid = uid; - results.user.settings = settings; + results = { + username: results.user.username, + userslug: results.user.userslug, + uid: uid, + yourid: req.user.uid, + theirid: uid, + settings: settings, + languages: results.languages + }; res.render('accountsettings', results); }); diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 41e8618344..953a06291f 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -37,7 +37,8 @@ adminController.home = function(req, res, next) { res.render('admin/index', { version: pkg.version, emailerInstalled: plugins.hasListeners('action:email.send'), - searchInstalled: plugins.hasListeners('filter:search.query') + searchInstalled: plugins.hasListeners('filter:search.query'), + restartRequired: meta.restartRequired }); }; diff --git a/src/controllers/categories.js b/src/controllers/categories.js index f30dcc13b3..07f6bf9ab0 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -68,14 +68,14 @@ categoriesController.get = function(req, res, next) { async.waterfall([ function(next) { categoryTools.privileges(cid, uid, function(err, categoryPrivileges) { - if (!err) { - if (!categoryPrivileges.read) { - next(new Error('[[error:no-privileges]]')); - } else { - next(null, categoryPrivileges); - } + if (err) { + return next(err); + } + + if (!categoryPrivileges.read) { + next(new Error('[[error:no-privileges]]')); } else { - next(err); + next(null, categoryPrivileges); } }); }, diff --git a/src/meta.js b/src/meta.js index 6837619cfe..26f84262df 100644 --- a/src/meta.js +++ b/src/meta.js @@ -18,7 +18,9 @@ var fs = require('fs'), User = require('./user'); (function (Meta) { + Meta.restartRequired = false; Meta.config = {}; + Meta.configs = { init: function (callback) { delete Meta.config; @@ -159,6 +161,9 @@ var fs = require('fs'), db.setObjectField('config', 'theme:src', data.src, callback); break; } + + // Restart Required flag + meta.restartRequired = true; } }; diff --git a/src/plugins.js b/src/plugins.js index 16e421ce62..4d56fd94d4 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -383,6 +383,8 @@ var fs = require('fs'), return; } + // Restart Required flag + meta.restartRequired = true; if(active) { Plugins.fireHook('action:plugin.deactivate', id); diff --git a/src/routes/debug.js b/src/routes/debug.js index 6c03961251..ca7a13b665 100644 --- a/src/routes/debug.js +++ b/src/routes/debug.js @@ -54,7 +54,6 @@ module.exports = function(app, middleware, controllers) { }); app.get('/test', function(req, res) { - require('../meta').sounds.init(); res.send(200); }); }); diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 64fbb841ba..05286f616b 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -79,7 +79,6 @@ SocketAdmin.themes.set = function(socket, data, callback) { widgets.reset(function(err) { meta.themes.set(data, function() { callback(); - meta.restart(); }); }); }; @@ -87,7 +86,6 @@ SocketAdmin.themes.set = function(socket, data, callback) { SocketAdmin.plugins.toggle = function(socket, plugin_id) { plugins.toggleActive(plugin_id, function(status) { socket.emit('admin.plugins.toggle', status); - meta.restart(); }); }; diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 80af126f29..3687424bd5 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -1,6 +1,7 @@ 'use strict'; -var categories = require('../categories'), +var async = require('async'), + categories = require('../categories'), categoryTools = require('../categoryTools'), meta = require('./../meta'), user = require('./../user'), @@ -30,12 +31,29 @@ SocketCategories.loadMore = function(socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - user.getSettings(socket.uid, function(err, settings) { + async.parallel({ + privileges: function(next) { + categoryTools.privileges(data.cid, socket.uid, next); + }, + settings: function(next) { + user.getSettings(socket.uid, next); + } + }, function(err, results) { + if (err) { + return callback(err); + } var start = parseInt(data.after, 10), - end = start + settings.topicsPerPage - 1; + end = start + results.settings.topicsPerPage - 1; + + categories.getCategoryTopics(data.cid, start, end, socket.uid, function(err, data) { + if (err) { + return callback(err); + } - categories.getCategoryTopics(data.cid, start, end, socket.uid, callback); + data.privileges = results.privileges; + callback(null, data); + }); }); }; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 401d09a8e9..9d8d233c64 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -194,7 +194,7 @@ SocketModules.chats.send = function(socket, data, callback) { function sendChatNotification(fromuid, touid, username) { if (!module.parent.exports.isUserOnline(touid)) { - var notifText = 'New message from ' + username + ''; + var notifText = '[[notifications:new_message_from,' + username + ']]'; notifications.create({ text: notifText, path: 'javascript:app.openChat('' + username + '', ' + fromuid + ');', diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index f2144896cd..dde786af7e 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -33,14 +33,11 @@ SocketPosts.reply = function(socket, data, callback) { } if (postData) { - - module.parent.exports.emitTopicPostStats(); - - var socketData = { + websockets.server.sockets.emit('event:new_post', { posts: [postData] - }; + }); - websockets.server.sockets.emit('event:new_post', socketData); + module.parent.exports.emitTopicPostStats(); callback(); } @@ -49,7 +46,7 @@ SocketPosts.reply = function(socket, data, callback) { SocketPosts.upvote = function(socket, data, callback) { favouriteCommand('upvote', 'voted', socket, data, callback); - sendNotificationToPostOwner(data, socket.uid, 'has upvoted your post'); + sendNotificationToPostOwner(data, socket.uid, 'notifications:upvoted_your_post'); }; SocketPosts.downvote = function(socket, data, callback) { @@ -62,7 +59,7 @@ SocketPosts.unvote = function(socket, data, callback) { SocketPosts.favourite = function(socket, data, callback) { favouriteCommand('favourite', 'favourited', socket, data, callback); - sendNotificationToPostOwner(data, socket.uid, 'has favourited your post'); + sendNotificationToPostOwner(data, socket.uid, 'notifications:favourited_your_post'); }; SocketPosts.unfavourite = function(socket, data, callback) { @@ -87,7 +84,7 @@ function favouriteCommand(command, eventName, socket, data, callback) { } } -function sendNotificationToPostOwner(data, uid, message) { +function sendNotificationToPostOwner(data, uid, notification) { if(data && data.pid && uid) { posts.getPostFields(data.pid, ['tid', 'uid'], function(err, postData) { if (err) { @@ -109,8 +106,9 @@ function sendNotificationToPostOwner(data, uid, message) { if (err) { return; } + notifications.create({ - text: '' + results.username + ' ' + message, + text: '[[' + notification + ', ' + results.username + ']]', path: nconf.get('relative_path') + '/topic/' + results.slug + '#' + data.pid, uniqueId: 'post:' + data.pid, from: uid @@ -258,7 +256,7 @@ SocketPosts.flag = function(socket, pid, callback) { user.getUserField(socket.uid, 'username', next); }, function(username, next) { - message = '' + username + ' flagged a post.'; + message = '[[notifications:user_flagged_post, ' + username + ']]'; posts.getPostField(pid, 'tid', next); }, function(tid, next) { diff --git a/src/threadTools.js b/src/threadTools.js index 565bf93ec6..d2de95bb3f 100644 --- a/src/threadTools.js +++ b/src/threadTools.js @@ -229,7 +229,7 @@ var winston = require('winston'), } notifications.create({ - text: '' + username + ' has posted a reply to: "' + topicData.title + '"', + text: '[[notifications:user_posted_to, ' + username + ', ' + topicData.title + ']]', path: nconf.get('relative_path') + '/topic/' + topicData.slug + '#' + pid, uniqueId: 'topic:' + tid, from: exceptUid diff --git a/src/user/create.js b/src/user/create.js index 87c90e4076..37cff32d72 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -49,7 +49,6 @@ module.exports = function(User) { } if (exists) { async.forever(function(next) { - // Append a random number to the username var newUsername = userData.username + (Math.floor(Math.random() * 255) + 1); User.exists(newUsername, function(err, exists) { if (!exists) { @@ -87,10 +86,11 @@ module.exports = function(User) { if (err) { return callback(err); } - userData = results[results.length - 1]; + userData = results[results.length - 1]; + var userNameChanged = !!results[3]; // If a new username was picked... - if (results[3]) { + if (userNameChanged) { userData.username = results[3]; userData.userslug = utils.slugify(results[3]); } @@ -149,16 +149,13 @@ module.exports = function(User) { groups.join('registered-users', uid); - // If their username was automatically changed... - if (results[3]) { - translator.translate('[[user:username_taken_workaround, ' + userData.username + ']]', function(notifText) { - notifications.create({ - text: notifText, - picture: 'brand:logo', - datetime: Date.now() - }, function(nid) { - notifications.push(nid, uid); - }); + if (userNameChanged) { + notifications.create({ + text: '[[user:username_taken_workaround, ' + userData.username + ']]', + picture: 'brand:logo', + datetime: Date.now() + }, function(nid) { + notifications.push(nid, uid); }); } diff --git a/src/user/jobs.js b/src/user/jobs.js index 602b889039..c2e88e752f 100644 --- a/src/user/jobs.js +++ b/src/user/jobs.js @@ -8,7 +8,6 @@ var db = require('../database'), nconf = require('nconf'), user = require('../user'), - UserNotifications = require('./notifications'), topics = require('../topics'), emailer = require('../emailer'), meta = require('../meta'); @@ -23,6 +22,8 @@ module.exports = function(User) { }; User.sendDailyDigests = function() { + var yesterday = Date.now() - (1000*60*60*24); + async.parallel({ recent: function(next) { topics.getLatestTopics(0, 0, 10, 'day', next); @@ -33,39 +34,61 @@ module.exports = function(User) { }, function(err, data) { var now = new Date(); - async.each(data.uids, function(uid, next) { - UserNotifications.getDailyUnread(uid, function(err, notifications) { - if (!err && notifications && notifications.length) { + async.parallel({ + recipients: function(next) { + User.getMultipleUserFields(data.uids, ['uid', 'username', 'lastonline'], next); + }, + userSettings: function(next) { + User.getMultipleUserSettings(data.uids, next); + } + }, function(err, users) { + var recipients = users.recipients, + userSettings = users.userSettings, + subscribed; + // Find uids subscribed to daily digest emails + subscribed = userSettings.filter(function(setting) { + return !setting.dailyDigestFreq || setting.dailyDigestFreq === 'daily'; + }).map(function(setting) { + return setting.uid; + }); + + // Find only those users who have not been online in the past 24 hours + var users = recipients.filter(function(userObj) { + return subscribed.indexOf(userObj.uid) !== -1 && yesterday > parseInt(userObj.lastonline, 10); + }); + + // Consider using eachLimit, but *only* if people complain about email relays choking -- otherwise we're ok. + async.each(users, function(userObj, next) { + user.notifications.getDailyUnread(userObj.uid, function(err, notifications) { + // Turn relative URLs into absolute ones for(var i=0; i made a new post'; + var message = '[[notifications:user_made_post, ' + username + ']]'; notifications.create({ text: message, diff --git a/src/user/settings.js b/src/user/settings.js index ab80e4fb42..781b1934f5 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -25,6 +25,7 @@ module.exports = function(User) { settings = data.settings; settings.showemail = settings.showemail ? parseInt(settings.showemail, 10) !== 0 : false; + settings.dailyDigestFreq = settings.dailyDigestFreq || 'daily'; settings.usePagination = settings.usePagination ? parseInt(settings.usePagination, 10) === 1 : parseInt(meta.config.usePagination, 10) === 1; settings.topicsPerPage = settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : parseInt(meta.config.topicsPerPage, 10) || 20; settings.postsPerPage = settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : parseInt(meta.config.postsPerPage, 10) || 10; @@ -35,16 +36,43 @@ module.exports = function(User) { }); }; + User.getMultipleUserSettings = function(uids, callback) { + if (!Array.isArray(uids) || !uids.length) { + return callback(null, []); + } + + var keys = uids.map(function(uid) { + return 'user:' + uid + ':settings'; + }); + + db.getObjects(keys, function(err, settings) { + if (err) { + return callback(err); + } + + // Associate uid + settings = settings.map(function(setting, idx) { + setting = setting || {}; + setting.uid = uids[idx]; + return setting; + }); + + callback(null, settings); + }); + }; + User.saveSettings = function(uid, data, callback) { if(!data.topicsPerPage || !data.postsPerPage || parseInt(data.topicsPerPage, 10) <= 0 || parseInt(data.postsPerPage, 10) <= 0) { return callback(new Error('[[error:invalid-pagination-value]]')); } - plugins.fireHook('action:user.saveSettings', {uid: uid, settings: data}); + data.language = data.language || meta.config.defaultLang; + plugins.fireHook('action:user.saveSettings', {uid: uid, settings: data}); db.setObject('user:' + uid + ':settings', { showemail: data.showemail, + dailyDigestFreq: data.dailyDigestFreq || 'daily', usePagination: data.usePagination, topicsPerPage: data.topicsPerPage, postsPerPage: data.postsPerPage,