diff --git a/app.js b/app.js index 26fe077f87..b47935376d 100644 --- a/app.js +++ b/app.js @@ -17,7 +17,6 @@ along with this program. If not, see . */ - "use strict"; /*global require, global, process*/ @@ -29,6 +28,7 @@ var fs = require('fs'), semver = require('semver'), winston = require('winston'), path = require('path'), + cluster = require('cluster'), pkg = require('./package.json'), utils = require('./public/src/utils.js'); @@ -103,6 +103,7 @@ function loadConfig() { } function start() { + loadConfig(); winston.info('Time: ' + new Date()); @@ -136,17 +137,38 @@ function start() { upgrade.check(function(schema_ok) { if (schema_ok || nconf.get('check-schema') === false) { sockets.init(webserver.server); - plugins.init(); nconf.set('url', nconf.get('base_url') + (nconf.get('use_port') ? ':' + nconf.get('port') : '') + nconf.get('relative_path')); plugins.ready(function() { - webserver.init(); + webserver.init(function() { + // If this callback is called, this means that loader.js is used + process.on('message', function(msg) { + if (msg === 'bind') { + webserver.listen(); + } + }); + process.send({ + action: 'ready' + }); + }); }); process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown); process.on('SIGHUP', restart); + process.on('message', function(message) { + switch(message.action) { + case 'reload': + meta.reload(); + break; + case 'js-propagate': + meta.js.cache = message.cache; + meta.js.map = message.map; + winston.info('[cluster] Client-side javascript and mapping propagated to worker ' + cluster.worker.id); + break; + } + }) process.on('uncaughtException', function(err) { winston.error(err.message); console.log(err.stack); @@ -313,6 +335,8 @@ function shutdown(code) { winston.info('[app] Shutdown (SIGTERM/SIGINT) Initialised.'); require('./src/database').close(); winston.info('[app] Database connection closed.'); + require('./src/webserver').server.close(); + winston.info('[app] Web server closed to connections.'); winston.info('[app] Shutdown complete.'); process.exit(code || 0); diff --git a/bcrypt.js b/bcrypt.js new file mode 100644 index 0000000000..a926310923 --- /dev/null +++ b/bcrypt.js @@ -0,0 +1,30 @@ + +'use strict'; + + +var bcrypt = require('bcryptjs'); + +process.on('message', function(m) { + if (m.type === 'hash') { + hash(m.rounds, m.password); + } else if (m.type === 'compare') { + compare(m.password, m.hash); + } +}); + +function hash(rounds, password) { + bcrypt.genSalt(rounds, function(err, salt) { + if (err) { + return process.send({type:'hash', err: {message: err.message}}); + } + bcrypt.hash(password, salt, function(err, hash) { + process.send({type:'hash', err: err ? {message: err.message} : null, hash: hash, password: password}); + }); + }); +} + +function compare(password, hash) { + bcrypt.compare(password, hash, function(err, res) { + process.send({type:'compare', err: err ? {message: err.message} : null, hash: hash, password: password, result: res}); + }); +} \ No newline at end of file diff --git a/install/data/defaults.json b/install/data/defaults.json index 32175b1312..15267aeee1 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -11,6 +11,10 @@ "field": "postDelay", "value": 10 }, + { + "field": "initialPostDelay", + "value": 10 + }, { "field": "minimumPostLength", "value": 8 @@ -31,6 +35,10 @@ "field": "allowLocalLogin", "value": 1 }, + { + "field": "allowAccountDelete", + "value": 1, + }, { "field": "allowFileUploads", "value": 0 @@ -68,8 +76,8 @@ "value": 256 }, { - "field": "chatMessagesToDisplay", - "value": 50 + "field": "profileImageDimension", + "value": 128 }, { "field": "requireEmailConfirmation", diff --git a/loader.js b/loader.js index 6448615348..777be064d8 100644 --- a/loader.js +++ b/loader.js @@ -2,81 +2,166 @@ var nconf = require('nconf'), fs = require('fs'), + path = require('path'), + cluster = require('cluster'), + async = require('async'), pidFilePath = __dirname + '/pidfile', output = fs.openSync(__dirname + '/logs/output.log', 'a'), - start = function() { - var fork = require('child_process').fork, - nbb_start = function() { - if (timesStarted > 3) { - console.log('\n[loader] Experienced three start attempts in 10 seconds, most likely an error on startup. Halting.'); - return nbb_stop(); - } + numCPUs, + Loader = { + timesStarted: 0, + shutdown_queue: [], + js: { + cache: undefined, + map: undefined + } + }; - timesStarted++; - if (startTimer) { - clearTimeout(startTimer); - } - startTimer = setTimeout(resetTimer, 1000*10); +Loader.init = function() { + cluster.setupMaster({ + exec: "app.js", + silent: false + }); - nbb = fork('./app', process.argv.slice(2), { - env: process.env - }); + cluster.on('fork', function(worker) { + worker.on('message', function(message) { + if (message && typeof message === 'object' && message.action) { + switch (message.action) { + case 'ready': + if (Loader.js.cache) { + worker.send({ + action: 'js-propagate', + cache: Loader.js.cache, + map: Loader.js.map + }); + } + + worker.send('bind'); - nbb.on('message', function(message) { - if (message && typeof message === 'object' && message.action) { - if (message.action === 'restart') { - nbb_restart(); + // Kill an instance in the shutdown queue + var workerToKill = Loader.shutdown_queue.pop(); + if (workerToKill) { + cluster.workers[workerToKill].kill(); } - } - }); + break; + case 'restart': + console.log('[cluster] Restarting...'); + Loader.restart(function(err) { + console.log('[cluster] Restarting...'); + }); + break; + case 'reload': + console.log('[cluster] Reloading...'); + Loader.reload(); + break; + case 'js-propagate': + Loader.js.cache = message.cache; + Loader.js.map = message.map; - nbb.on('exit', function(code, signal) { - if (code) { - nbb_start(); - } else { - nbb_stop(); - } - }); - }, - nbb_stop = function() { - if (startTimer) { - clearTimeout(startTimer); + var otherWorkers = Object.keys(cluster.workers).filter(function(worker_id) { + return parseInt(worker_id, 10) !== parseInt(worker.id, 10); + }); + otherWorkers.forEach(function(worker_id) { + cluster.workers[worker_id].send({ + action: 'js-propagate', + cache: message.cache, + map: message.map + }); + }); + break; + case 'listening': + if (message.primary) { + Loader.primaryWorker = parseInt(worker.id, 10); + } + break; + case 'user:connect': + case 'user:disconnect': + notifyWorkers(worker, message); + break; } + } + }); + }); + + cluster.on('listening', function(worker) { + console.log('[cluster] Child Process (' + worker.process.pid + ') listening for connections.'); + }); + + function notifyWorkers(currentWorker, msg) { + Object.keys(cluster.workers).forEach(function(id) { + cluster.workers[id].send(msg); + }); + } - nbb.kill(); - if (fs.existsSync(pidFilePath)) { - var pid = parseInt(fs.readFileSync(pidFilePath, { encoding: 'utf-8' }), 10); - if (process.pid === pid) { - fs.unlinkSync(pidFilePath); - } + cluster.on('exit', function(worker, code, signal) { + if (code !== 0) { + if (Loader.timesStarted < numCPUs*3) { + Loader.timesStarted++; + if (Loader.crashTimer) { + clearTimeout(Loader.crashTimer); } - }, - nbb_restart = function() { - nbb.removeAllListeners('exit').on('exit', function() { - nbb_start(); + Loader.crashTimer = setTimeout(function() { + Loader.timesStarted = 0; }); - nbb.kill(); - }, - resetTimer = function() { - clearTimeout(startTimer); - timesStarted = 0; - }, - timesStarted = 0, - startTimer; - - process.on('SIGINT', nbb_stop); - process.on('SIGTERM', nbb_stop); - process.on('SIGHUP', nbb_restart); - - nbb_start(); - }, - nbb; - -nconf.argv(); - -// Start the daemon! + } else { + console.log(numCPUs*3, 'restarts in 10 seconds, most likely an error on startup. Halting.'); + process.exit(); + } + } + + console.log('[cluster] Child Process (' + worker.process.pid + ') has exited (code: ' + code + ')'); + if (!worker.suicide) { + console.log('[cluster] Spinning up another process...') + + var wasPrimary = parseInt(worker.id, 10) === Loader.primaryWorker; + cluster.fork({ + handle_jobs: wasPrimary + }); + } + }); + + process.on('SIGHUP', Loader.restart); + + Loader.start(); +}; + +Loader.start = function() { + Loader.primaryWorker = 1; + + for(var x=0;xلا توجد مواضيع في هذه الفئةلماذا لا تحاول نشر موضوع؟
", "browsing": "تصفح", "no_replies": "لم يرد أحد", - "share_this_category": "انشر هذه الفئة" + "share_this_category": "انشر هذه الفئة", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/ar/error.json b/public/language/ar/error.json index eaa79e1cc5..6e6956b0ec 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "كلمة السر غير مقبولة", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "اسم المستخدم ماخوذ", "email-taken": "البريد الالكتروني ماخوذ", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "المستخدم محظور", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "مشكلة في الرفع: 1%", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/ar/modules.json b/public/language/ar/modules.json index 9f6267d3a4..1b740fb922 100644 --- a/public/language/ar/modules.json +++ b/public/language/ar/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/ar/notifications.json b/public/language/ar/notifications.json index 32d6b62dcf..eec61f2062 100644 --- a/public/language/ar/notifications.json +++ b/public/language/ar/notifications.json @@ -10,11 +10,14 @@ "new_notification": "New Notification", "you_have_unread_notifications": "You have unread notifications.", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/ar/recent.json b/public/language/ar/recent.json index fa2a081df6..866d0e28fc 100644 --- a/public/language/ar/recent.json +++ b/public/language/ar/recent.json @@ -4,5 +4,6 @@ "week": "أسبوع", "month": "شهر", "year": "Year", + "alltime": "All Time", "no_recent_topics": "There are no recent topics." } \ No newline at end of file diff --git a/public/language/ar/search.json b/public/language/ar/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/ar/search.json +++ b/public/language/ar/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/ar/user.json b/public/language/ar/user.json index 0439014857..01f1439527 100644 --- a/public/language/ar/user.json +++ b/public/language/ar/user.json @@ -4,6 +4,8 @@ "username": "إسم المستخدم", "email": "البريد الإلكتروني", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "الاسم الكامل", "website": "الموقع الإلكتروني", "location": "موقع", @@ -62,5 +64,7 @@ "posts_per_page": "Posts per Page", "notification_sounds": "Play a sound when you receive a notification.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/cs/category.json b/public/language/cs/category.json index fcaf82c582..0c5e3af859 100644 --- a/public/language/cs/category.json +++ b/public/language/cs/category.json @@ -3,5 +3,6 @@ "no_topics": "V této kategorii zatím nejsou žádné příspěvky.
Můžeš být první!", "browsing": "prohlíží", "no_replies": "Nikdo ještě neodpověděl", - "share_this_category": "Share this category" + "share_this_category": "Share this category", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 7eba6e0ff9..f90d459cce 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json index 33299f001b..cb79a5a156 100644 --- a/public/language/cs/modules.json +++ b/public/language/cs/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/cs/notifications.json b/public/language/cs/notifications.json index e315bfd6c0..8a77183b5e 100644 --- a/public/language/cs/notifications.json +++ b/public/language/cs/notifications.json @@ -10,11 +10,14 @@ "new_notification": "New Notification", "you_have_unread_notifications": "You have unread notifications.", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/cs/recent.json b/public/language/cs/recent.json index 34933d6d13..d769997c6f 100644 --- a/public/language/cs/recent.json +++ b/public/language/cs/recent.json @@ -4,5 +4,6 @@ "week": "Týden", "month": "Měsíc", "year": "Year", + "alltime": "All Time", "no_recent_topics": "There are no recent topics." } \ No newline at end of file diff --git a/public/language/cs/search.json b/public/language/cs/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/cs/search.json +++ b/public/language/cs/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/cs/user.json b/public/language/cs/user.json index 4ffdafbc7f..1f1230bde3 100644 --- a/public/language/cs/user.json +++ b/public/language/cs/user.json @@ -4,6 +4,8 @@ "username": "Uživatelské jméno", "email": "Email", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Jméno a příjmení", "website": "Webové stránky", "location": "Poloha", @@ -62,5 +64,7 @@ "posts_per_page": "Posts per Page", "notification_sounds": "Play a sound when you receive a notification.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/de/category.json b/public/language/de/category.json index df8b9796ef..f455b1604b 100644 --- a/public/language/de/category.json +++ b/public/language/de/category.json @@ -3,5 +3,6 @@ "no_topics": "Es gibt noch keine Themen in dieser Kategorie.
Warum beginnst du nicht das erste?", "browsing": "Aktiv", "no_replies": "Niemand hat geantwortet", - "share_this_category": "Teile diese Kategorie" + "share_this_category": "Teile diese Kategorie", + "ignore": "Ignorieren" } \ No newline at end of file diff --git a/public/language/de/modules.json b/public/language/de/modules.json index 4a19291085..215f0838cf 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Nachrichtenverlauf", "chat.pop-out": "Chat als Pop-out anzeigen", "chat.maximize": "Maximieren", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 sagte in %2:", "composer.user_said": "%1 sagte:", "composer.discard": "Bist du sicher, dass du diesen Post verwerfen möchtest?" diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json index 295796f5d6..7140887186 100644 --- a/public/language/de/notifications.json +++ b/public/language/de/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Neue Benachrichtigung", "you_have_unread_notifications": "Du hast ungelesene Benachrichtigungen.", "new_message_from": "Neue Nachricht von %1", - "upvoted_your_post": "%1 hat deinen Beitrag positiv bewertet.", - "favourited_your_post": "%1 favorisiert deinen Beitrag.", - "user_flagged_post": "%1 hat einen Beitrag markiert.", + "upvoted_your_post_in": "%1 hat deinen Beitrag in %2 positiv bewertet.", + "moved_your_post": "%1 hat deinen Beitrag verschoben.", + "moved_your_topic": "%1 hat dein Thema verschoben.", + "favourited_your_post_in": "%1 hat deinen Beitrag in %2 favorisiert.", + "user_flagged_post_in": "%1 hat einen Beitrag in
%2
gemeldet", "user_posted_to": "%1 hat auf %2 geantwortet.", "user_mentioned_you_in": "%1 erwähnte dich in %2", + "user_started_following_you": "%1 folgt dir jetzt.", "email-confirmed": "E-Mail bestätigt", "email-confirmed-message": "Vielen Dank für Ihre E-Mail-Validierung. Ihr Konto ist nun vollständig aktiviert.", "email-confirm-error": "Es ist ein Fehler aufgetreten ...", diff --git a/public/language/de/pages.json b/public/language/de/pages.json index c49854a2a4..2df893a878 100644 --- a/public/language/de/pages.json +++ b/public/language/de/pages.json @@ -5,7 +5,7 @@ "recent": "Neueste Themen", "users": "Registrierte User", "notifications": "Benachrichtigungen", - "tags": "Topics tagged under \"%1\"", + "tags": "Themen markiert unter \"%1\"", "user.edit": "Bearbeite \"%1\"", "user.following": "Nutzer, die %1 folgt", "user.followers": "Nutzer, die %1 folgen", diff --git a/public/language/de/recent.json b/public/language/de/recent.json index b428875786..dfed232f64 100644 --- a/public/language/de/recent.json +++ b/public/language/de/recent.json @@ -4,5 +4,6 @@ "week": "Woche", "month": "Monat", "year": "Jahr", + "alltime": "Gesamter Zeitraum", "no_recent_topics": "Es gibt keine aktuellen Themen." } \ No newline at end of file diff --git a/public/language/de/search.json b/public/language/de/search.json index 47ca0be16b..4141710537 100644 --- a/public/language/de/search.json +++ b/public/language/de/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 Ergebniss(e) stimmen mit \"%2\" überein, (%3 Sekunden)" + "results_matching": "%1 Ergebniss(e) stimmen mit \"%2\" überein, (%3 Sekunden)", + "no-matches": "Keine Beiträge gefunden" } \ No newline at end of file diff --git a/public/language/de/topic.json b/public/language/de/topic.json index 86b6f20d38..77fbaf000f 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -87,7 +87,7 @@ "more_users_and_guests": "%1 weitere(r) Nutzer und %2 Gäste", "more_users": "%1 weitere(r) Nutzer", "more_guests": "%1 weitere Gäste", - "users_and_others": "%1 and %2 others", + "users_and_others": "%1 und %2 andere", "sort_by": "Sortieren nach", "oldest_to_newest": "Älteste zuerst", "newest_to_oldest": "Neuster zuerst", diff --git a/public/language/en@pirate/category.json b/public/language/en@pirate/category.json index 44e19baa01..74b946d216 100644 --- a/public/language/en@pirate/category.json +++ b/public/language/en@pirate/category.json @@ -3,5 +3,6 @@ "no_topics": "Thar be no topics in 'tis category.
Why don't ye give a go' postin' one?", "browsing": "browsin'", "no_replies": "No one has replied to ye message", - "share_this_category": "Share this category" + "share_this_category": "Share this category", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/en@pirate/error.json b/public/language/en@pirate/error.json index 7eba6e0ff9..f90d459cce 100644 --- a/public/language/en@pirate/error.json +++ b/public/language/en@pirate/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/en@pirate/modules.json b/public/language/en@pirate/modules.json index b0f32fba74..661d150639 100644 --- a/public/language/en@pirate/modules.json +++ b/public/language/en@pirate/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/en@pirate/notifications.json b/public/language/en@pirate/notifications.json index cc1705eff6..49a3a10e05 100644 --- a/public/language/en@pirate/notifications.json +++ b/public/language/en@pirate/notifications.json @@ -10,11 +10,14 @@ "new_notification": "New Notification", "you_have_unread_notifications": "You have unread notifications.", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/en@pirate/recent.json b/public/language/en@pirate/recent.json index 6e6df209fa..bb4318cb39 100644 --- a/public/language/en@pirate/recent.json +++ b/public/language/en@pirate/recent.json @@ -4,5 +4,6 @@ "week": "Week", "month": "Month", "year": "Year", + "alltime": "All Time", "no_recent_topics": "There be no recent topics." } \ No newline at end of file diff --git a/public/language/en@pirate/search.json b/public/language/en@pirate/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/en@pirate/search.json +++ b/public/language/en@pirate/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/en@pirate/user.json b/public/language/en@pirate/user.json index 624ff3c60f..6492a2ab79 100644 --- a/public/language/en@pirate/user.json +++ b/public/language/en@pirate/user.json @@ -4,6 +4,8 @@ "username": "User Name", "email": "Email", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Full Name", "website": "Website", "location": "Location", @@ -62,5 +64,7 @@ "posts_per_page": "Posts per Page", "notification_sounds": "Play a sound when you receive a notification.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/en_GB/category.json b/public/language/en_GB/category.json index db742469f9..22d72c3064 100644 --- a/public/language/en_GB/category.json +++ b/public/language/en_GB/category.json @@ -5,5 +5,6 @@ "browsing": "browsing", "no_replies": "No one has replied", - "share_this_category": "Share this category" + "share_this_category": "Share this category", + "ignore": "Ignore" } diff --git a/public/language/en_GB/email.json b/public/language/en_GB/email.json index 87d3a5be39..bfcf159de8 100644 --- a/public/language/en_GB/email.json +++ b/public/language/en_GB/email.json @@ -17,10 +17,15 @@ "digest.latest_topics": "Latest topics from %1", "digest.cta": "Click here to visit %1", "digest.unsub.info": "This digest was sent to you due to your subscription settings.", - "digest.unsub.cta": "Click here to alter those settings", "digest.daily.no_topics": "There have been no active topics in the past day", + "notif.chat.subject": "New chat message received from %1", + "notif.chat.cta": "Click here to continue the conversation", + "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", + "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", + "unsub.cta": "Click here to alter those settings", + "closing": "Thanks!" } \ No newline at end of file diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index 2b9a0aec46..7c4baf4cc4 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -15,6 +15,8 @@ "invalid-title": "Invalid title", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", + "invalid-search-term": "Invalid search term", "invalid-pagination-value": "Invalid pagination value", @@ -23,8 +25,10 @@ "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", + "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", "no-topic": "Topic doesn't exist", @@ -75,5 +79,10 @@ "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/en_GB/modules.json b/public/language/en_GB/modules.json index 9bf107bec6..12e6d7457f 100644 --- a/public/language/en_GB/modules.json +++ b/public/language/en_GB/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", diff --git a/public/language/en_GB/notifications.json b/public/language/en_GB/notifications.json index b45129b467..3cb6ac6872 100644 --- a/public/language/en_GB/notifications.json +++ b/public/language/en_GB/notifications.json @@ -12,11 +12,14 @@ "you_have_unread_notifications": "You have unread notifications.", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to" : "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", diff --git a/public/language/en_GB/recent.json b/public/language/en_GB/recent.json index 32b31d2666..ff6400f27a 100644 --- a/public/language/en_GB/recent.json +++ b/public/language/en_GB/recent.json @@ -4,5 +4,6 @@ "week": "Week", "month": "Month", "year": "Year", + "alltime": "All Time", "no_recent_topics": "There are no recent topics." } \ No newline at end of file diff --git a/public/language/en_GB/search.json b/public/language/en_GB/search.json index 20d27c2a46..81c404a591 100644 --- a/public/language/en_GB/search.json +++ b/public/language/en_GB/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/en_GB/tags.json b/public/language/en_GB/tags.json index 49adc42b2c..f67b2ca7b5 100644 --- a/public/language/en_GB/tags.json +++ b/public/language/en_GB/tags.json @@ -2,5 +2,6 @@ "no_tag_topics": "There are no topics with this tag.", "tags": "Tags", "enter_tags_here": "Enter tags here. Press enter after each tag.", + "enter_tags_here_short": "Enter tags...", "no_tags": "There are no tags yet." } \ No newline at end of file diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index c6fe5bc4ad..de892fb800 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -33,7 +33,7 @@ "flag_title": "Flag this post for moderation", "flag_confirm": "Are you sure you want to flag this post?", "flag_success": "This post has been flagged for moderation.", - "deleted_message": "This thread has been deleted. Only users with thread management privileges can see it.", + "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", "not_following_topic.message": "You will no longer receive notifications from this topic.", @@ -46,7 +46,7 @@ "watch.title": "Be notified of new replies in this topic", "share_this_post": "Share this Post", - "thread_tools.title": "Thread Tools", + "thread_tools.title": "Topic Tools", "thread_tools.markAsUnreadForAll": "Mark Unread", "thread_tools.pin": "Pin Topic", "thread_tools.unpin": "Unpin Topic", @@ -56,11 +56,11 @@ "thread_tools.move_all": "Move All", "thread_tools.fork": "Fork Topic", "thread_tools.delete": "Delete Topic", - "thread_tools.delete_confirm": "Are you sure you want to delete this thread?", + "thread_tools.delete_confirm": "Are you sure you want to delete this topic?", "thread_tools.restore": "Restore Topic", - "thread_tools.restore_confirm": "Are you sure you want to restore this thread?", + "thread_tools.restore_confirm": "Are you sure you want to restore this topic?", "thread_tools.purge": "Purge Topic", - "thread_tools.purge_confirm" : "Are you sure you want to purge this thread?", + "thread_tools.purge_confirm" : "Are you sure you want to purge this topic?", "topic_move_success": "This topic has been successfully moved to %1", @@ -86,7 +86,7 @@ "topic_will_be_moved_to": "This topic will be moved to the category", "fork_topic_instruction": "Click the posts you want to fork", "fork_no_pids": "No posts selected!", - "fork_success": "Succesfully forked topic!", + "fork_success": "Succesfully forked topic! Click here to go to the forked topic.", "composer.title_placeholder": "Enter your topic title here...", "composer.discard": "Discard", diff --git a/public/language/en_GB/user.json b/public/language/en_GB/user.json index ab8c1cdae6..2948c47292 100644 --- a/public/language/en_GB/user.json +++ b/public/language/en_GB/user.json @@ -5,6 +5,8 @@ "email": "Email", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Full Name", "website": "Website", @@ -55,6 +57,7 @@ "digest_daily": "Daily", "digest_weekly": "Weekly", "digest_monthly": "Monthly", + "send_chat_notifications": "Send an email if a new chat message arrives and I am not online", "has_no_follower": "This user doesn't have any followers :(", "follows_no_one": "This user isn't following anyone :(", @@ -71,5 +74,8 @@ "notification_sounds" : "Play a sound when you receive a notification.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } diff --git a/public/language/en_US/category.json b/public/language/en_US/category.json index 99ba34a172..93272e1a06 100644 --- a/public/language/en_US/category.json +++ b/public/language/en_US/category.json @@ -3,5 +3,6 @@ "no_topics": "There are no topics in this category.
Why don't you try posting one?", "browsing": "browsing", "no_replies": "No one has replied", - "share_this_category": "Share this category" + "share_this_category": "Share this category", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/en_US/error.json b/public/language/en_US/error.json index 794cd19db0..c63073194a 100644 --- a/public/language/en_US/error.json +++ b/public/language/en_US/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/en_US/modules.json b/public/language/en_US/modules.json index 0567700071..9fbc634fef 100644 --- a/public/language/en_US/modules.json +++ b/public/language/en_US/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/en_US/notifications.json b/public/language/en_US/notifications.json index 7f97d92275..4d2461226d 100644 --- a/public/language/en_US/notifications.json +++ b/public/language/en_US/notifications.json @@ -10,11 +10,14 @@ "new_notification": "New Notification", "you_have_unread_notifications": "You have unread notifications.", "new_message_from": "New message from %1", - "upvoted_your_post": "%1 has upvoted your post.", - "favourited_your_post": "%1 has favorited your post.", - "user_flagged_post": "%1 flagged a post.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/en_US/recent.json b/public/language/en_US/recent.json index 1bf0cf9f01..d683cce436 100644 --- a/public/language/en_US/recent.json +++ b/public/language/en_US/recent.json @@ -4,5 +4,6 @@ "week": "Week", "month": "Month", "year": "Year", + "alltime": "All Time", "no_recent_topics": "There are no recent topics." } \ No newline at end of file diff --git a/public/language/en_US/search.json b/public/language/en_US/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/en_US/search.json +++ b/public/language/en_US/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/en_US/user.json b/public/language/en_US/user.json index b711845230..cdb91a91d1 100644 --- a/public/language/en_US/user.json +++ b/public/language/en_US/user.json @@ -4,6 +4,8 @@ "username": "User Name", "email": "Email", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Full Name", "website": "Website", "location": "Location", @@ -62,5 +64,7 @@ "posts_per_page": "Posts per Page", "notification_sounds": "Play a sound when you receive a notification.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/es/category.json b/public/language/es/category.json index cd64161deb..7146b3eed2 100644 --- a/public/language/es/category.json +++ b/public/language/es/category.json @@ -3,5 +3,6 @@ "no_topics": "No hay temas en esta categoría.
¿Por que no te animas y publicas uno?", "browsing": "viendo ahora", "no_replies": "Nadie ha respondido aún", - "share_this_category": "Compartir esta categoría" + "share_this_category": "Compartir esta categoría", + "ignore": "Ignorar" } \ No newline at end of file diff --git a/public/language/es/email.json b/public/language/es/email.json index c83494b565..bc973d1e34 100644 --- a/public/language/es/email.json +++ b/public/language/es/email.json @@ -14,10 +14,10 @@ "digest.cta": "Cliquea aquí para visitar %1", "digest.unsub.info": "Este compendio te fue enviado debido a tus ajustes de subscripción.", "digest.daily.no_topics": "No han habido temas activos en el día pasado", - "notif.chat.subject": "New chat message received from %1", - "notif.chat.cta": "Click here to continue the conversation", - "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", + "notif.chat.subject": "Nuevo mensaje de chat recibido de %1", + "notif.chat.cta": "Haz click aquí para continuar la conversación", + "notif.chat.unsub.info": "Esta notificación de chat se te envió debido a tus ajustes de suscripción.", "test.text1": "Este es un email de prueba para verificar que el envío de email está ajustado correctamente para tu NodeBB", - "unsub.cta": "Click here to alter those settings", + "unsub.cta": "Haz click aquí para modificar los ajustes.", "closing": "¡Gracias!" } \ No newline at end of file diff --git a/public/language/es/error.json b/public/language/es/error.json index 130fd6217d..d4c30e6d91 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -12,13 +12,15 @@ "invalid-title": "Título no válido!", "invalid-user-data": "Datos de Usuario no válidos", "invalid-password": "Contraseña no válida", + "invalid-username-or-password": "Por favor especifica tanto un usuario como contraseña", "invalid-pagination-value": "Valor de paginación no válido.", "username-taken": "Nombre de usuario ya escogido", "email-taken": "El correo electrónico ya está escogido.", "email-not-confirmed": "Tu correo electrónico está sin confirmar, por favor haz click aquí para confirmar tu email.", "username-too-short": "El nombre de usuario es demasiado corto", + "username-too-long": "Nombre de usuario demasiado largo", "user-banned": "Usuario expulsado", - "user-too-new": "You need to wait %1 seconds before making your first post!", + "user-too-new": "Necesitas esperar %1 segundos antes de hacer tu primera publicación.", "no-category": "La categoría no existe", "no-topic": "El tema no existe.", "no-post": "La publicación no existe", @@ -26,7 +28,7 @@ "no-user": "El usuario no existe", "no-teaser": "El extracto del tema no existe.", "no-privileges": "No tienes los privilegios necesarios para esa acción.", - "no-emailers-configured": "No email plugins were loaded, so a test email could not be sent", + "no-emailers-configured": "Ningún plugin para email fue cargado, así que no se pudo enviar email de prueba.", "category-disabled": "Categoría deshabilitada.", "topic-locked": "Tema bloqueado.", "still-uploading": "Por favor, espera a que terminen las subidas.", @@ -53,5 +55,9 @@ "upload-error": "Error de subida: %1", "signature-too-long": "Las firmas no pueden ser más largas de %1 caracteres!", "cant-chat-with-yourself": "No puedes conversar contigo mismo!", - "not-enough-reputation-to-downvote": "No tienes suficiente reputación para votar negativo este post" + "reputation-system-disabled": "El sistema de reputación está deshabilitado.", + "downvoting-disabled": "La votación negativa está deshabilitada.", + "not-enough-reputation-to-downvote": "No tienes suficiente reputación para votar negativo este post", + "not-enough-reputation-to-flag": "No tienes suficiente reputación para marcar esta publicación", + "reload-failed": "NodeBB encontró un problema mientras refrescar: \"%1\". NodeBB intentará cargar el resto de contenido, aunque deberías deshacer lo que hiciste antes de refrescar." } \ No newline at end of file diff --git a/public/language/es/groups.json b/public/language/es/groups.json index c00c111e11..bb4b46d1f8 100644 --- a/public/language/es/groups.json +++ b/public/language/es/groups.json @@ -1,7 +1,7 @@ { - "view_group": "View Group", - "details.title": "Group Details", - "details.members": "Member List", - "details.has_no_posts": "This group's members have not made any posts.", - "details.latest_posts": "Latest Posts" + "view_group": "Ver Grupo", + "details.title": "Detalles de Grupo", + "details.members": "Lista de Miembros", + "details.has_no_posts": "Los miembros de este grupo no han hecho ninguna publicación.", + "details.latest_posts": "Últimas Publicaciones" } \ No newline at end of file diff --git a/public/language/es/modules.json b/public/language/es/modules.json index ea478bb904..94a1481fff 100644 --- a/public/language/es/modules.json +++ b/public/language/es/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Historial de mensajes", "chat.pop-out": "Mostrar en ventana independiente", "chat.maximize": "Maximizar", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 dijo en %2:", "composer.user_said": "%1 dijo:", "composer.discard": "¿Estás seguro de que deseas descargar este post?" diff --git a/public/language/es/notifications.json b/public/language/es/notifications.json index 3a7168a44d..3be3cccdc8 100644 --- a/public/language/es/notifications.json +++ b/public/language/es/notifications.json @@ -4,17 +4,20 @@ "see_all": "Ver todas las notificaciones", "back_to_home": "Volver a %1", "outgoing_link": "Enlace Externo", - "outgoing_link_message": "You are now leaving %1.", - "continue_to": "Continue to %1", - "return_to": "Return to %1", + "outgoing_link_message": "Ahora estás saliendo %1.", + "continue_to": "Continuar a %1", + "return_to": "Regresar a %1", "new_notification": "Nueva Notificación", "you_have_unread_notifications": "Tienes notificaciones sin leer.", "new_message_from": "Nuevo mensaje de %1", - "upvoted_your_post": "%1 ha marcado como favorita tu respuesta.", - "favourited_your_post": "%1 ha marcado como favorita tu respuesta.", - "user_flagged_post": "%1 ha marcado como indebida una respuesta.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 ha publicado una respuesta a: %2", "user_mentioned_you_in": "%1 te mencionó en %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Correo electrónico confirmado", "email-confirmed-message": "Gracias por validar tu correo electrónico. Tu cuenta ya está completamente activa.", "email-confirm-error": "Un error ocurrió...", diff --git a/public/language/es/pages.json b/public/language/es/pages.json index 4b79bc293f..ab6e4eaedd 100644 --- a/public/language/es/pages.json +++ b/public/language/es/pages.json @@ -5,7 +5,7 @@ "recent": "Temas Recientes", "users": "Usuarios Registrado", "notifications": "Notificaciones", - "tags": "Topics tagged under \"%1\"", + "tags": "Temas etiquetados bajo \"%1\"", "user.edit": "Editando \"%1\"", "user.following": "Gente que sigue %1 ", "user.followers": "Seguidores de %1", diff --git a/public/language/es/recent.json b/public/language/es/recent.json index 8b25e63d46..a187f9d661 100644 --- a/public/language/es/recent.json +++ b/public/language/es/recent.json @@ -4,5 +4,6 @@ "week": "Semana", "month": "Mes", "year": "Año", + "alltime": "All Time", "no_recent_topics": "No hay publicaciones recientes" } \ No newline at end of file diff --git a/public/language/es/search.json b/public/language/es/search.json index d0ffc64f36..5cd357b2e9 100644 --- a/public/language/es/search.json +++ b/public/language/es/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 resuldado(s) coinciden con \"%2\". (%3 segundos)", + "no-matches": "No se encontraron publicaciones" } \ No newline at end of file diff --git a/public/language/es/topic.json b/public/language/es/topic.json index d80f000183..d8eafd749c 100644 --- a/public/language/es/topic.json +++ b/public/language/es/topic.json @@ -87,7 +87,7 @@ "more_users_and_guests": "%1 usuario(s) y %2 invitado(s) más", "more_users": "%1 usuario(s) más", "more_guests": "%1 invitado(s) más", - "users_and_others": "%1 and %2 others", + "users_and_others": "%1 y otros %2", "sort_by": "Ordenar por", "oldest_to_newest": "Más antiguo a más nuevo", "newest_to_oldest": "Más nuevo a más antiguo", diff --git a/public/language/es/user.json b/public/language/es/user.json index 7b20401772..4c2f89a501 100644 --- a/public/language/es/user.json +++ b/public/language/es/user.json @@ -4,6 +4,8 @@ "username": "Nombre de usuario", "email": "Correo Electrónico", "confirm_email": "Repetir correo electrónico", + "delete_account": "Eliminar cuenta", + "delete_account_confirm": "Estás seguro de que quieres eliminar tu cuenta?
Esta acción es irreversible y no podrás recuperar tus datos

Introduce tu nombre de usuario para confirmar la eliminación de la cuenta.", "fullname": "Nombre completo", "website": "Sitio Web", "location": "Ubicación", @@ -50,7 +52,7 @@ "digest_daily": "Diariamente", "digest_weekly": "Semanalmente", "digest_monthly": "Mensualmente", - "send_chat_notifications": "Send an email if a new chat message arrives and I am not online", + "send_chat_notifications": "Envía un correo electrónico si recibes un mensaje de chat cuando no estás en línea.", "has_no_follower": "Este miembro no tiene seguidores. :(", "follows_no_one": "Este miembro no sigue a nadie. :(", "has_no_posts": "Este usuario aún no ha publicado nada.", @@ -62,5 +64,7 @@ "posts_per_page": "Post por página", "notification_sounds": "Reproducir un sonido al recibir una notificación", "browsing": "Preferencias de navegación.", - "open_links_in_new_tab": "Abrir los enlaces externos en una nueva pestaña?" + "open_links_in_new_tab": "Abrir los enlaces externos en una nueva pestaña?", + "follow_topics_you_reply_to": "Seguir publicaciones en las que respondes.", + "follow_topics_you_create": "Seguir publicaciones que creas." } \ No newline at end of file diff --git a/public/language/et/category.json b/public/language/et/category.json index 1d35948aac..865d8dbae9 100644 --- a/public/language/et/category.json +++ b/public/language/et/category.json @@ -3,5 +3,6 @@ "no_topics": "Kahjuks ei leidu siin kategoorias ühtegi teemat.
Soovid postitada?", "browsing": "vaatab", "no_replies": "Keegi pole vastanud", - "share_this_category": "Jaga seda kategooriat" + "share_this_category": "Jaga seda kategooriat", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/et/error.json b/public/language/et/error.json index bb555c84d6..51926c86b5 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -12,11 +12,13 @@ "invalid-title": "Vigane pealkiri!", "invalid-user-data": "Vigased kasutaja andmed", "invalid-password": "Vigane parool", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Vigane lehe väärtus", "username-taken": "Kasutajanimi on juba võetud", "email-taken": "Email on võetud", "email-not-confirmed": "Su emaili aadress ei ole kinnitatud, vajuta siia et kinnitada.", "username-too-short": "Kasutajanimi on liiga lühike", + "username-too-long": "Username too long", "user-banned": "Kasutaja bannitud", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Kategooriat ei eksisteeri", @@ -53,5 +55,9 @@ "upload-error": "Üleslaadimise viga: %1", "signature-too-long": "Allkiri ei saa olla pikem kui %1 tähemärki!", "cant-chat-with-yourself": "Sa ei saa endaga vestelda!", - "not-enough-reputation-to-downvote": "Sul ei ole piisavalt reputatsiooni, et anda negatiivset hinnangut sellele postitusele." + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "Sul ei ole piisavalt reputatsiooni, et anda negatiivset hinnangut sellele postitusele.", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/et/modules.json b/public/language/et/modules.json index 18ff7ecfff..ad67cbb689 100644 --- a/public/language/et/modules.json +++ b/public/language/et/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Sõnumite ajalugu", "chat.pop-out": "Pop-out vestlus", "chat.maximize": "Suurenda", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 ütles %2:", "composer.user_said": "%1 ütles:", "composer.discard": "Oled kindel, et soovid selle postituse tühistada?" diff --git a/public/language/et/notifications.json b/public/language/et/notifications.json index 4de13a6e0f..8ee69d3072 100644 --- a/public/language/et/notifications.json +++ b/public/language/et/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Uus teade", "you_have_unread_notifications": "Sul ei ole lugemata teateid.", "new_message_from": "Uus sõnum kasutajalt %1", - "upvoted_your_post": "%1 hääletas sinu postituse poolt.", - "favourited_your_post": "%1 märkis sinu postituse lemmikuks.", - "user_flagged_post": "%1 märgistas postituse.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "Kasutaja %1 postitas vastuse teemasse %2", "user_mentioned_you_in": "%1 mainis sind postituses %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Emaili aadress kinnitatud", "email-confirmed-message": "Täname, et kinnitasite oma emaili aadressi. Teie kasutaja omn nüüd täielikult aktiveeritud.", "email-confirm-error": "Süsteemis tekkis viga...", diff --git a/public/language/et/recent.json b/public/language/et/recent.json index 7afd6c41cd..53c112df2d 100644 --- a/public/language/et/recent.json +++ b/public/language/et/recent.json @@ -4,5 +4,6 @@ "week": "Nädal", "month": "Kuu", "year": "Aasta", + "alltime": "All Time", "no_recent_topics": "Hetkel ei ole hiljutisi teemasid." } \ No newline at end of file diff --git a/public/language/et/search.json b/public/language/et/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/et/search.json +++ b/public/language/et/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/et/user.json b/public/language/et/user.json index 86592bfe3c..9decf2a23e 100644 --- a/public/language/et/user.json +++ b/public/language/et/user.json @@ -4,6 +4,8 @@ "username": "Kasutajanimi", "email": "Email", "confirm_email": "Kinnita email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Täisnimi", "website": "Koduleht", "location": "Asukoht", @@ -62,5 +64,7 @@ "posts_per_page": "Postitusi ühe lehekülje kohta", "notification_sounds": "Tee häält, kui saabub teade.", "browsing": "Sirvimis sätted", - "open_links_in_new_tab": "Ava väljaminevad lingid uues vaheaknas?" + "open_links_in_new_tab": "Ava väljaminevad lingid uues vaheaknas?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/fa_IR/category.json b/public/language/fa_IR/category.json index 1f5299e534..c1c61a1a9f 100644 --- a/public/language/fa_IR/category.json +++ b/public/language/fa_IR/category.json @@ -3,5 +3,6 @@ "no_topics": "هیچ جستاری در این دسته نیست.
چرا شما یکی نفرستید؟", "browsing": "بیننده‌ها", "no_replies": "هیچ کسی پاسخ نداده است.", - "share_this_category": "به اشتراک‌گذاری این دسته" + "share_this_category": "به اشتراک‌گذاری این دسته", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/fa_IR/error.json b/public/language/fa_IR/error.json index b3cb689a27..64f4d35823 100644 --- a/public/language/fa_IR/error.json +++ b/public/language/fa_IR/error.json @@ -12,11 +12,13 @@ "invalid-title": "عنوان نامعتبر است!", "invalid-user-data": "داده‌های کاربری نامعتبر است.", "invalid-password": "گذرواژه نامعتبر است.", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "عدد صفحه‌بندی نامعتبر است.", "username-taken": "این نام کاربری گرفته شده است.", "email-taken": "این رایانامه گرفته شده است.", "email-not-confirmed": "رایانامه شما تأیید نشده است، لطفاً برای تأیید رایانامه‌تان اینجا را بفشارید.", "username-too-short": "نام کاربری خیلی کوتاه است.", + "username-too-long": "Username too long", "user-banned": "کاربر محروم شد.", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "چنین دسته‌ای وجود ندارد.", @@ -53,5 +55,9 @@ "upload-error": "خطای بارگذاری: %1", "signature-too-long": "امضا نمی‌تواند بیش‌تر از %1 نویسه داشته باشد.", "cant-chat-with-yourself": "شما نمی‌توانید با خودتان گفتگو کنید!", - "not-enough-reputation-to-downvote": "شما اعتبار کافی برای دادن رای منفی به این دیدگاه را ندارید." + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "شما اعتبار کافی برای دادن رای منفی به این دیدگاه را ندارید.", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/fa_IR/modules.json b/public/language/fa_IR/modules.json index 4d22d32319..fdb9473d36 100644 --- a/public/language/fa_IR/modules.json +++ b/public/language/fa_IR/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "تاریخچه پیام‌ها", "chat.pop-out": "Pop out chat", "chat.maximize": "تمام صفحه", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 در %2 گفته است:", "composer.user_said": "%1 گفته است:", "composer.discard": "آیا از دور انداختن این دیدگاه اطمینان دارید؟" diff --git a/public/language/fa_IR/notifications.json b/public/language/fa_IR/notifications.json index a643005e04..62b48b71af 100644 --- a/public/language/fa_IR/notifications.json +++ b/public/language/fa_IR/notifications.json @@ -10,11 +10,14 @@ "new_notification": "آکاه‌سازی تازه", "you_have_unread_notifications": "شما آگاه‌سازی‌های نخوانده دارید.", "new_message_from": "پیام تازه از %1", - "upvoted_your_post": "%1 به دیدگاه شما رای داده است.", - "favourited_your_post": "%1 دیدگاه شما را پسندیده است.", - "user_flagged_post": "پرچم خوردن یک دیدگاه از سوی %1", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "پاسخ دادن به %2 از سوی %1", "user_mentioned_you_in": "%1 در %2 به شما اشاره کرد", + "user_started_following_you": "%1 started following you.", "email-confirmed": "رایانامه تایید شد", "email-confirmed-message": "بابت تایید ایمیلتان سپاس‌گزاریم. حساب کاربری شما اکنون به صورت کامل فعال شده است.", "email-confirm-error": "خطایی پیش آمده است...", diff --git a/public/language/fa_IR/recent.json b/public/language/fa_IR/recent.json index 74a8876ebf..4f6f3256a5 100644 --- a/public/language/fa_IR/recent.json +++ b/public/language/fa_IR/recent.json @@ -4,5 +4,6 @@ "week": "هفته", "month": "ماه", "year": "سال", + "alltime": "All Time", "no_recent_topics": "هیچ جستار تازه‌ای نیست." } \ No newline at end of file diff --git a/public/language/fa_IR/search.json b/public/language/fa_IR/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/fa_IR/search.json +++ b/public/language/fa_IR/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/fa_IR/user.json b/public/language/fa_IR/user.json index 63c9836722..e6085077d4 100644 --- a/public/language/fa_IR/user.json +++ b/public/language/fa_IR/user.json @@ -4,6 +4,8 @@ "username": "نام کاربری", "email": "رایانامه", "confirm_email": "تأیید رایانامه", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "نام کامل", "website": "تارنما", "location": "محل سکونت", @@ -62,5 +64,7 @@ "posts_per_page": "شمار دیدگاه‌ها در هر برگه", "notification_sounds": "پخش صدا هنگامی که شما یک آگاه‌سازی دریافت می‌کنید.", "browsing": "تنظیمات مرور", - "open_links_in_new_tab": "بازکردن لینک‌های خارجی در تب جدید؟" + "open_links_in_new_tab": "بازکردن لینک‌های خارجی در تب جدید؟", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/fi/category.json b/public/language/fi/category.json index 67c3c2125a..f3ae42f69f 100644 --- a/public/language/fi/category.json +++ b/public/language/fi/category.json @@ -3,5 +3,6 @@ "no_topics": "Tällä aihealueella ei ole yhtään aihetta.
Miksi et aloittaisi uutta?", "browsing": "selaamassa", "no_replies": "Kukaan ei ole vastannut", - "share_this_category": "Jaa tämä kategoria" + "share_this_category": "Jaa tämä kategoria", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/fi/error.json b/public/language/fi/error.json index d1c8b2b46c..73febab0c3 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -12,11 +12,13 @@ "invalid-title": "Virheellinen otsikko!", "invalid-user-data": "Virheellinen käyttäjätieto", "invalid-password": "Virheellinen salasana", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Virheellinen taittoarvo", "username-taken": "Käyttäjänimi varattu", "email-taken": "Sähköpostiosoite varattu", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Käyttäjänimi on liian lyhyt", + "username-too-long": "Username too long", "user-banned": "Käyttäjä on estetty", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Kategoriaa ei ole olemassa", @@ -53,5 +55,9 @@ "upload-error": "Lähetysvirhe: %1", "signature-too-long": "Allekirjoitus ei voi olla pidempi kuin %1 merkkiä!", "cant-chat-with-yourself": "Et voi keskustella itsesi kanssa!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/fi/modules.json b/public/language/fi/modules.json index 2493032dca..bc64dfc9a3 100644 --- a/public/language/fi/modules.json +++ b/public/language/fi/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/fi/notifications.json b/public/language/fi/notifications.json index e9507997a0..2eb5163d4a 100644 --- a/public/language/fi/notifications.json +++ b/public/language/fi/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Uusi ilmoitus", "you_have_unread_notifications": "Sinulla on lukemattomia ilmoituksia.", "new_message_from": "Uusi viesti käyttäjältä %1", - "upvoted_your_post": "%1 has upvoted your post.", - "favourited_your_post": "%1 lisäsi viestisi suosikkeihinsa.", - "user_flagged_post": "%1 flagged a post.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Sähköpostiosoite vahvistettu", "email-confirmed-message": "Kiitos sähköpostiosoitteesi vahvistamisesta. Käyttäjätilisi on nyt täysin aktivoitu.", "email-confirm-error": "Tapahtui virhe...", diff --git a/public/language/fi/recent.json b/public/language/fi/recent.json index fb3bd49dbf..b2bff39da7 100644 --- a/public/language/fi/recent.json +++ b/public/language/fi/recent.json @@ -4,5 +4,6 @@ "week": "Viikko", "month": "Kuukausi", "year": "Year", + "alltime": "All Time", "no_recent_topics": "Ei viimeisimpiä aiheita." } \ No newline at end of file diff --git a/public/language/fi/search.json b/public/language/fi/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/fi/search.json +++ b/public/language/fi/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/fi/user.json b/public/language/fi/user.json index bb02df8532..930f9cffb0 100644 --- a/public/language/fi/user.json +++ b/public/language/fi/user.json @@ -4,6 +4,8 @@ "username": "Käyttäjän nimi", "email": "Sähköposti", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Koko nimi", "website": "Kotisivu", "location": "Sijainti", @@ -62,5 +64,7 @@ "posts_per_page": "Viestiä per sivu", "notification_sounds": "Soita merkkiääni ilmoituksen saapuessa.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/fr/error.json b/public/language/fr/error.json index 6c83792006..bcc272f6d8 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -12,11 +12,13 @@ "invalid-title": "Titre invalide !", "invalid-user-data": "Données utilisateur invalides", "invalid-password": "Mot de passe invalide", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Valeur de pagination invalide", "username-taken": "Nom d’utilisateur déjà utilisé", "email-taken": "Email déjà utilisé", "email-not-confirmed": "Votre adresse email n'est pas confirmée, cliquez ici pour la valider.", "username-too-short": "Nom d'utilisateur trop court", + "username-too-long": "Username too long", "user-banned": "Utilisateur banni", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Cette catégorie n'existe pas", @@ -53,5 +55,9 @@ "upload-error": "Erreur d'envoi : %1", "signature-too-long": "La signature ne peut dépasser %1 caractères !", "cant-chat-with-yourself": "Vous ne pouvez chatter avec vous même !", - "not-enough-reputation-to-downvote": "Vous n'avez pas une réputation assez élevée pour noter négativement ce message" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "Vous n'avez pas une réputation assez élevée pour noter négativement ce message", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json index 1b2adba55d..9b20cbd5e8 100644 --- a/public/language/fr/modules.json +++ b/public/language/fr/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Historique des messages", "chat.pop-out": "Afficher la discussion", "chat.maximize": "Agrandir", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 a dit dans %2 :", "composer.user_said": "%1 a dit :", "composer.discard": "Êtes-vous sûr de bien vouloir supprimer ce message ?" diff --git a/public/language/fr/notifications.json b/public/language/fr/notifications.json index 560fb83751..5e1178f78c 100644 --- a/public/language/fr/notifications.json +++ b/public/language/fr/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Nouvelle notification", "you_have_unread_notifications": "Vous avez des notifications non-lues", "new_message_from": "Nouveau message de %1", - "upvoted_your_post": "%1 a voté pour votre message.", - "favourited_your_post": "%1 a mis votre message en favoris.", - "user_flagged_post": "%1 a signalé un message.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 a répondu à : %2", "user_mentioned_you_in": "%1 vous a mentionné dans %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email vérifié", "email-confirmed-message": "Merci pour la validation de votre adresse email. Votre compte est désormais activé.", "email-confirm-error": "Un erreur est survenue ...", diff --git a/public/language/fr/recent.json b/public/language/fr/recent.json index 2e69d07a4e..6af7b3e81a 100644 --- a/public/language/fr/recent.json +++ b/public/language/fr/recent.json @@ -4,5 +4,6 @@ "week": "Semaine", "month": "Mois", "year": "An", + "alltime": "All Time", "no_recent_topics": "Il n'y a aucun sujet récent." } \ No newline at end of file diff --git a/public/language/fr/search.json b/public/language/fr/search.json index ab88db2e52..8974b7f593 100644 --- a/public/language/fr/search.json +++ b/public/language/fr/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 résultat(s) correspondant(s) à \"%2\", (%3 secondes)" + "results_matching": "%1 résultat(s) correspondant(s) à \"%2\", (%3 secondes)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/fr/user.json b/public/language/fr/user.json index ffd192f285..a476ad9f05 100644 --- a/public/language/fr/user.json +++ b/public/language/fr/user.json @@ -4,6 +4,8 @@ "username": "Nom d'utilisateur", "email": "Email", "confirm_email": "Confirmer l'adresse email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Nom", "website": "Site web", "location": "Emplacement", @@ -62,5 +64,7 @@ "posts_per_page": "Messages par page", "notification_sounds": "Émettre un son lors de la réception de notifications.", "browsing": "Paramètres de navigation", - "open_links_in_new_tab": "Ouvrir les liens externes dans un nouvel onglet ?" + "open_links_in_new_tab": "Ouvrir les liens externes dans un nouvel onglet ?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/he/category.json b/public/language/he/category.json index 4d32d01bea..bc5e530729 100644 --- a/public/language/he/category.json +++ b/public/language/he/category.json @@ -3,5 +3,6 @@ "no_topics": "קטגוריה זו ריקה מנושאים.
למה שלא תנסה להוסיף נושא חדש?", "browsing": "צופים בנושא זה כעת", "no_replies": "אין תגובות", - "share_this_category": "שתף קטגוריה זו" + "share_this_category": "שתף קטגוריה זו", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/he/error.json b/public/language/he/error.json index 1a2e967f3f..28cd04e663 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -12,11 +12,13 @@ "invalid-title": "כותרת שגויה", "invalid-user-data": "מידע משתמש שגוי", "invalid-password": "סיסמא שגויה", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "ערך דפדוף שגוי", "username-taken": "שם משתמש תפוס", "email-taken": "כתובת אימייל תפוסה", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "שם משתמש קצר מדי", + "username-too-long": "Username too long", "user-banned": "המשתמש חסום", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "קטגוריה אינה קיימת", @@ -53,5 +55,9 @@ "upload-error": "שגיאה בהעלאה : %1", "signature-too-long": "חתימה אינה יכולה להיות ארוכה מ %1 תווים!", "cant-chat-with-yourself": "לא ניתן לעשות צ'אט עם עצמך!", - "not-enough-reputation-to-downvote": "אין לך מספיק מוניטין כדי להוריד את הדירוג של פוסט זה" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "אין לך מספיק מוניטין כדי להוריד את הדירוג של פוסט זה", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/he/modules.json b/public/language/he/modules.json index 918929e838..072459c69b 100644 --- a/public/language/he/modules.json +++ b/public/language/he/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 אמר ב%2:", "composer.user_said": "%1 אמר:", "composer.discard": "האם למחוק פוסט זה?" diff --git a/public/language/he/notifications.json b/public/language/he/notifications.json index bcbb5eac8b..f3f8efa00f 100644 --- a/public/language/he/notifications.json +++ b/public/language/he/notifications.json @@ -10,11 +10,14 @@ "new_notification": "New Notification", "you_have_unread_notifications": "You have unread notifications.", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/he/recent.json b/public/language/he/recent.json index 5629c9f23f..0a20c6301a 100644 --- a/public/language/he/recent.json +++ b/public/language/he/recent.json @@ -4,5 +4,6 @@ "week": "שבוע", "month": "חודש", "year": "Year", + "alltime": "All Time", "no_recent_topics": "אין נושאים חדשים" } \ No newline at end of file diff --git a/public/language/he/search.json b/public/language/he/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/he/search.json +++ b/public/language/he/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/he/user.json b/public/language/he/user.json index d6ae814b03..58ee8081e5 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -4,6 +4,8 @@ "username": "שם משתמש", "email": "כתובת אימייל", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "שם מלא", "website": "אתר", "location": "מיקום", @@ -62,5 +64,7 @@ "posts_per_page": "כמות פוסטים בעמוד", "notification_sounds": "השמע צליל כאשר מתקבלת הודעה עבורך.", "browsing": "הגדרות צפייה", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/hu/category.json b/public/language/hu/category.json index f6f1002ad9..f0eb8a400b 100644 --- a/public/language/hu/category.json +++ b/public/language/hu/category.json @@ -3,5 +3,6 @@ "no_topics": "Még nincs nyitva egy téma sem ebben a kategóriában.Miért nem hozol létre egyet?", "browsing": "jelenlévők", "no_replies": "Senki sem válaszolt még", - "share_this_category": "Hozzászólás megosztása" + "share_this_category": "Hozzászólás megosztása", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/hu/error.json b/public/language/hu/error.json index 7eba6e0ff9..f90d459cce 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/hu/modules.json b/public/language/hu/modules.json index 646de2be03..556ee5812c 100644 --- a/public/language/hu/modules.json +++ b/public/language/hu/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/hu/notifications.json b/public/language/hu/notifications.json index c189a4e5bb..86ce42f88a 100644 --- a/public/language/hu/notifications.json +++ b/public/language/hu/notifications.json @@ -10,11 +10,14 @@ "new_notification": "New Notification", "you_have_unread_notifications": "You have unread notifications.", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/hu/recent.json b/public/language/hu/recent.json index 332aaf1de2..b595ffd958 100644 --- a/public/language/hu/recent.json +++ b/public/language/hu/recent.json @@ -4,5 +4,6 @@ "week": "Hét", "month": "Hónap", "year": "Year", + "alltime": "All Time", "no_recent_topics": "Nincs friss topik." } \ No newline at end of file diff --git a/public/language/hu/search.json b/public/language/hu/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/hu/search.json +++ b/public/language/hu/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/hu/user.json b/public/language/hu/user.json index 594ea3df95..a7ef8fc8c0 100644 --- a/public/language/hu/user.json +++ b/public/language/hu/user.json @@ -4,6 +4,8 @@ "username": "Felhasználónév", "email": "E-mail", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Teljes Név", "website": "Weboldal", "location": "Lakhely", @@ -62,5 +64,7 @@ "posts_per_page": "Hozzászólás oldalanként", "notification_sounds": "Hang lejátszása ha értesítés érkezett.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/it/category.json b/public/language/it/category.json index 4d813335c3..c1f4ace222 100644 --- a/public/language/it/category.json +++ b/public/language/it/category.json @@ -3,5 +3,6 @@ "no_topics": "Non ci sono discussioni in questa categoria.
Perché non ne inizi una?", "browsing": "visualizzando", "no_replies": "Nessuno ha risposto", - "share_this_category": "Share this category" + "share_this_category": "Share this category", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/it/error.json b/public/language/it/error.json index 7eba6e0ff9..f90d459cce 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/it/modules.json b/public/language/it/modules.json index b29af37317..6ad72bc915 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json index 1d2e122b74..957afe9eec 100644 --- a/public/language/it/notifications.json +++ b/public/language/it/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Nuove Notifiche", "you_have_unread_notifications": "Hai notifiche non lette.", "new_message_from": "Nuovo messaggio da %1", - "upvoted_your_post": "%1 has upvoted your post.", - "favourited_your_post": "%1 has favourited your post.", - "user_flagged_post": "%1 flagged a post.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/it/recent.json b/public/language/it/recent.json index 56a1191bc0..2047204baf 100644 --- a/public/language/it/recent.json +++ b/public/language/it/recent.json @@ -4,5 +4,6 @@ "week": "Settimana", "month": "Mese", "year": "Year", + "alltime": "All Time", "no_recent_topics": "Non ci sono discussioni recenti." } \ No newline at end of file diff --git a/public/language/it/search.json b/public/language/it/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/it/search.json +++ b/public/language/it/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/it/user.json b/public/language/it/user.json index 6f8f35a5c4..c06f211a6f 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -4,6 +4,8 @@ "username": "Nome Utente", "email": "Email", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Nome e Cognome", "website": "Sito Internet", "location": "Località", @@ -62,5 +64,7 @@ "posts_per_page": "Post per Pagina", "notification_sounds": "Play a sound when you receive a notification.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/ja/category.json b/public/language/ja/category.json index 43bc8f22d5..c5ffc01c13 100644 --- a/public/language/ja/category.json +++ b/public/language/ja/category.json @@ -3,5 +3,6 @@ "no_topics": "まだスレッドはありません.
一番目のスレッドを書いてみないか?", "browsing": "閲覧中", "no_replies": "返事はまだありません", - "share_this_category": "この板を共有" + "share_this_category": "この板を共有", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/ja/error.json b/public/language/ja/error.json index 2269da3edf..0d6aad1afd 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "無効なユーザーデータ", "invalid-password": "無効なパスワード", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "無効な改ページ設定値", "username-taken": "ユーザー名が取られた", "email-taken": "メールアドレスが使用された", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "ユーザーが停止された", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "板が存在しない", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "署名は最大%1文字までです!", "cant-chat-with-yourself": "自分にチャットすることはできません!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/ja/modules.json b/public/language/ja/modules.json index 8f4d5713da..57f8c05db1 100644 --- a/public/language/ja/modules.json +++ b/public/language/ja/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/ja/notifications.json b/public/language/ja/notifications.json index efe8333fac..9823deff19 100644 --- a/public/language/ja/notifications.json +++ b/public/language/ja/notifications.json @@ -10,11 +10,14 @@ "new_notification": "新しい通知", "you_have_unread_notifications": "未読の通知があります。", "new_message_from": "%1からの新しいメッセージ", - "upvoted_your_post": "%1はあなたのポストを評価しました。", - "favourited_your_post": "%1はあなたのポストをお気に入りにしました。", - "user_flagged_post": "%1 ポストを報告しました。", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1%2 への返事を作成しました。", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/ja/recent.json b/public/language/ja/recent.json index 4ca75a039d..35cdd2b23f 100644 --- a/public/language/ja/recent.json +++ b/public/language/ja/recent.json @@ -4,5 +4,6 @@ "week": "最近 1 週", "month": "最近 1 ヶ月", "year": "Year", + "alltime": "All Time", "no_recent_topics": "最近のスレッドはありません。" } \ No newline at end of file diff --git a/public/language/ja/search.json b/public/language/ja/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/ja/search.json +++ b/public/language/ja/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/ja/user.json b/public/language/ja/user.json index 6af6fc3f8f..ba3b2c1ed0 100644 --- a/public/language/ja/user.json +++ b/public/language/ja/user.json @@ -4,6 +4,8 @@ "username": "ユーザー名", "email": "メール", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "フルネーム", "website": "ウェブサイト", "location": "ロケーション", @@ -62,5 +64,7 @@ "posts_per_page": "ページ毎のポスト数", "notification_sounds": "通知が来たとき音を流す", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/ko/category.json b/public/language/ko/category.json index 5437b07304..0ce8558aa9 100644 --- a/public/language/ko/category.json +++ b/public/language/ko/category.json @@ -3,5 +3,6 @@ "no_topics": "이 카테고리에는 생성된 주제가 없습니다.
먼저 주제를 생성해 보세요.", "browsing": "이 주제를 읽고 있는 사용자", "no_replies": "답글이 없습니다.", - "share_this_category": "이 카테고리를 공유" + "share_this_category": "이 카테고리를 공유", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/ko/error.json b/public/language/ko/error.json index c62181e62a..be2363f55a 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -12,11 +12,13 @@ "invalid-title": "올바르지 않은 제목입니다.", "invalid-user-data": "올바르지 않은 사용자 정보입니다.", "invalid-password": "올바르지 않은 비밀번호입니다.", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "올바르지 않은 페이지입니다.", "username-taken": "이미 사용 중인 사용자 이름입니다.", "email-taken": "이미 사용 중인 이메일입니다.", "email-not-confirmed": "아직 이메일이 인증되지 않았습니다. 여기를 누르면 인증 메일을 발송할 수 있습니다.", "username-too-short": "사용자 이름이 너무 짧습니다.", + "username-too-long": "Username too long", "user-banned": "차단된 사용자입니다.", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "존재하지 않는 카테고리입니다.", @@ -53,5 +55,9 @@ "upload-error": "업로드 오류가 발생했습니다. : %1", "signature-too-long": "서명은 최대 %1자로 제한됩니다.", "cant-chat-with-yourself": "자신과는 채팅할 수 없습니다.", - "not-enough-reputation-to-downvote": "인기도가 낮아 이 게시물에 반대할 수 없습니다." + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "인기도가 낮아 이 게시물에 반대할 수 없습니다.", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/ko/modules.json b/public/language/ko/modules.json index afadc4a16a..4b8f13b37f 100644 --- a/public/language/ko/modules.json +++ b/public/language/ko/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/ko/notifications.json b/public/language/ko/notifications.json index 8ececb6c49..90d004d7e0 100644 --- a/public/language/ko/notifications.json +++ b/public/language/ko/notifications.json @@ -10,11 +10,14 @@ "new_notification": "새 알림", "you_have_unread_notifications": "읽지 않은 알림이 있습니다.", "new_message_from": "%1님이 메시지를 보냈습니다.", - "upvoted_your_post": "%1님이 내 게시물을 추천했습니다.", - "favourited_your_post": "%1님이 내 게시물을 관심글로 등록했습니다.", - "user_flagged_post": "%1님이 게시물을 신고했습니다.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1님이 %2님의 게시물에 답글을 작성했습니다.", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/ko/recent.json b/public/language/ko/recent.json index 6190dda700..5e3f728004 100644 --- a/public/language/ko/recent.json +++ b/public/language/ko/recent.json @@ -4,5 +4,6 @@ "week": "주간", "month": "월간", "year": "Year", + "alltime": "All Time", "no_recent_topics": "최근 생성된 주제가 없습니다." } \ No newline at end of file diff --git a/public/language/ko/search.json b/public/language/ko/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/ko/search.json +++ b/public/language/ko/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/ko/user.json b/public/language/ko/user.json index 058840a767..37ef4e717d 100644 --- a/public/language/ko/user.json +++ b/public/language/ko/user.json @@ -4,6 +4,8 @@ "username": "사용자 이름", "email": "이메일", "confirm_email": "이메일 확인", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "이름", "website": "웹 사이트", "location": "거주지", @@ -62,5 +64,7 @@ "posts_per_page": "페이지 당 게시물 수", "notification_sounds": "알림을 수신하면 알림음을 재생", "browsing": "페이지 열기", - "open_links_in_new_tab": "외부 링크를 새 탭에서 열기" + "open_links_in_new_tab": "외부 링크를 새 탭에서 열기", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/lt/category.json b/public/language/lt/category.json index ba84df3cf4..1c9dd0899b 100644 --- a/public/language/lt/category.json +++ b/public/language/lt/category.json @@ -3,5 +3,6 @@ "no_topics": "Šioje kategorijoje temų nėra.
Kodėl gi jums nesukūrus naujos?", "browsing": "naršo", "no_replies": "Niekas dar neatsakė", - "share_this_category": "Pasidalinti šią kategoriją" + "share_this_category": "Pasidalinti šią kategoriją", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/lt/error.json b/public/language/lt/error.json index ddc3463ef8..48edfbb72a 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -12,11 +12,13 @@ "invalid-title": "Neteisingas pavadinimas!", "invalid-user-data": "Klaidingi vartotojo duomenys", "invalid-password": "Klaidingas slaptažodis", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Klaidinga puslapiavimo reikšmė", "username-taken": "Vartotojo vardas jau užimtas", "email-taken": "El. pašto adresas jau užimtas", "email-not-confirmed": "Jūsų el. paštas nepatvirtintas, prašome paspausti čia norint jį patvirtinti.", "username-too-short": "Slapyvardis per trumpas", + "username-too-long": "Username too long", "user-banned": "Vartotojas užblokuotas", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Kategorija neegzistuoja", @@ -53,5 +55,9 @@ "upload-error": "Įkėlimo klaida: %1", "signature-too-long": "Parašas negali būti ilgesnis negu %1 simboliai!", "cant-chat-with-yourself": "Jūs negalite susirašinėti su savimi!", - "not-enough-reputation-to-downvote": "Jūs neturite pakankamai reputacijos balsuoti prieš šį pranešimą" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "Jūs neturite pakankamai reputacijos balsuoti prieš šį pranešimą", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/lt/modules.json b/public/language/lt/modules.json index b64357c69d..872951166b 100644 --- a/public/language/lt/modules.json +++ b/public/language/lt/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Žinučių istorija", "chat.pop-out": "Iššokančio lango pokalbiai", "chat.maximize": "Padininti", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 parašė į %2:", "composer.user_said": "%1 parašė:", "composer.discard": "Ar tikrai norite sunaikinti šį pranešimą?" diff --git a/public/language/lt/notifications.json b/public/language/lt/notifications.json index 96ffad091b..864652cb57 100644 --- a/public/language/lt/notifications.json +++ b/public/language/lt/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Naujas pranešimas", "you_have_unread_notifications": "Jūs turite neperskaitytų pranešimų.", "new_message_from": "Nauja žinutė nuo %1", - "upvoted_your_post": "%1 teigiamai įvertino jūsų pranešimą.", - "favourited_your_post": "%1 pamėgo jūsų pranešimą.", - "user_flagged_post": "%1 pažymėjo pranešimą moderavimui.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 parašė atsaką %2", "user_mentioned_you_in": "%1 paminėjo Jus %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "El. paštas patvirtintas", "email-confirmed-message": "Dėkojame už el. pašto patvirtinimą. Jūsų paskyra pilnai aktyvuota.", "email-confirm-error": "Įvyko klaida...", diff --git a/public/language/lt/recent.json b/public/language/lt/recent.json index 5be1ef3ff5..0ccdef0f8c 100644 --- a/public/language/lt/recent.json +++ b/public/language/lt/recent.json @@ -4,5 +4,6 @@ "week": "Savaitė", "month": "Mėnesis", "year": "Metai", + "alltime": "All Time", "no_recent_topics": "Paskutinių temų nėra" } \ No newline at end of file diff --git a/public/language/lt/search.json b/public/language/lt/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/lt/search.json +++ b/public/language/lt/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/lt/user.json b/public/language/lt/user.json index f5ce19295c..2ef87f0c0c 100644 --- a/public/language/lt/user.json +++ b/public/language/lt/user.json @@ -4,6 +4,8 @@ "username": "Vartotojo vardas", "email": "El. paštas", "confirm_email": "Patvirtinti el. paštą", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Vardas ir pavardė", "website": "Tinklalapis", "location": "Vieta", @@ -62,5 +64,7 @@ "posts_per_page": "Pranešimų puslapyje", "notification_sounds": "Groti garsą kai gaunate pranešimą.", "browsing": "Naršymo nustatymai", - "open_links_in_new_tab": "Atidaryti nuorodas naujame skirtuke?" + "open_links_in_new_tab": "Atidaryti nuorodas naujame skirtuke?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/ms/category.json b/public/language/ms/category.json index 755fb94858..db2dc8d039 100644 --- a/public/language/ms/category.json +++ b/public/language/ms/category.json @@ -3,5 +3,6 @@ "no_topics": "Tiada topik dalam kategori ini.
Cuba menghantar topik yang baru?", "browsing": "melihat", "no_replies": "Tiada jawapan", - "share_this_category": "Kongsi kategori ini" + "share_this_category": "Kongsi kategori ini", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/ms/error.json b/public/language/ms/error.json index 2b74851c54..b6ad0bd586 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Password salah!", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Nama pengunna terlalu pendek", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/ms/modules.json b/public/language/ms/modules.json index 15ed55ba53..360bc5b729 100644 --- a/public/language/ms/modules.json +++ b/public/language/ms/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/ms/notifications.json b/public/language/ms/notifications.json index 530c9d1f93..02cc302284 100644 --- a/public/language/ms/notifications.json +++ b/public/language/ms/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Pemberitahuan baru", "you_have_unread_notifications": "Anda ada pemberitahuan yang belum dibaca", "new_message_from": "Pesanan baru daripada %1", - "upvoted_your_post": "%1 telah undi-naik posting anda", - "favourited_your_post": "strong>%1
telah menggemari posting anda", - "user_flagged_post": "%1 menandakan posting", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 telah membalas posting kepada: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/ms/recent.json b/public/language/ms/recent.json index 8342257702..bface6de72 100644 --- a/public/language/ms/recent.json +++ b/public/language/ms/recent.json @@ -4,5 +4,6 @@ "week": "Minggu", "month": "Bulan", "year": "Year", + "alltime": "All Time", "no_recent_topics": "Tiada topik terkini" } \ No newline at end of file diff --git a/public/language/ms/search.json b/public/language/ms/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/ms/search.json +++ b/public/language/ms/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/ms/user.json b/public/language/ms/user.json index 24aeaa0143..366c6ae8fb 100644 --- a/public/language/ms/user.json +++ b/public/language/ms/user.json @@ -4,6 +4,8 @@ "username": "Nama pengguna", "email": "Emel", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Nama penuh", "website": "Laman Web", "location": "Lokasi", @@ -62,5 +64,7 @@ "posts_per_page": "Kiriman setiap muka", "notification_sounds": "Mainkan muzik apabila anda menerima maklumbalas", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/nb/category.json b/public/language/nb/category.json index 14ab70de34..af2119d658 100644 --- a/public/language/nb/category.json +++ b/public/language/nb/category.json @@ -3,5 +3,6 @@ "no_topics": "Det er ingen emner i denne kategorien
Hvorfor ikke lage ett?", "browsing": "leser", "no_replies": "Ingen har svart", - "share_this_category": "Del denne kategorien" + "share_this_category": "Del denne kategorien", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/nb/error.json b/public/language/nb/error.json index 7eba6e0ff9..f90d459cce 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/nb/modules.json b/public/language/nb/modules.json index d6c4c7516d..17e695de33 100644 --- a/public/language/nb/modules.json +++ b/public/language/nb/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Meldingshistorikk", "chat.pop-out": "Pop-out chat", "chat.maximize": "Maksimer", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 sa i %2: ", "composer.user_said": "%1 sa: ", "composer.discard": "Er du sikker på at du vil forkaste dette innlegget?" diff --git a/public/language/nb/notifications.json b/public/language/nb/notifications.json index 96afebd800..2ffabea7c1 100644 --- a/public/language/nb/notifications.json +++ b/public/language/nb/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Nytt varsel", "you_have_unread_notifications": "Du har uleste varsler.", "new_message_from": "Ny melding fra %1", - "upvoted_your_post": "%1 har stemt opp ditt innlegg.", - "favourited_your_post": "%1 har favorittmerket ditt innlegg.", - "user_flagged_post": "%1 flagget ett innlegg.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 har skrevet et svar til: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/nb/recent.json b/public/language/nb/recent.json index 608c10c8b6..d3b44fc0f4 100644 --- a/public/language/nb/recent.json +++ b/public/language/nb/recent.json @@ -4,5 +4,6 @@ "week": "Uke", "month": "Måned", "year": "Year", + "alltime": "All Time", "no_recent_topics": "Det er ingen nye tråder." } \ No newline at end of file diff --git a/public/language/nb/search.json b/public/language/nb/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/nb/search.json +++ b/public/language/nb/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/nb/user.json b/public/language/nb/user.json index c94f0fd3d0..020d50b173 100644 --- a/public/language/nb/user.json +++ b/public/language/nb/user.json @@ -4,6 +4,8 @@ "username": "Brukernavn", "email": "E-post", "confirm_email": "Bekfreft e-post", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Fullt navn", "website": "Nettsted", "location": "Plassering", @@ -62,5 +64,7 @@ "posts_per_page": "Innlegg per side", "notification_sounds": "Spill av en lyd når du mottar ett varsel.", "browsing": "Surfeinnstillinger", - "open_links_in_new_tab": "Åpne utgående linker i en ny fane?" + "open_links_in_new_tab": "Åpne utgående linker i en ny fane?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 3543e1b6a4..d27566d742 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -9,7 +9,7 @@ "invalid-uid": "Ongeldig Gebruikers ID", "invalid-username": "Ongeldig Gebruikersnaam", "invalid-email": "Ongeldig Email Adres", - "invalid-title": "Ongeldig Titel!", + "invalid-title": "Ongeldige Titel!", "invalid-user-data": "Ongeldig Gebruikersdata", "invalid-password": "Ongeldig wachtwoord", "invalid-username-or-password": "Geef alsjeblieft een gebruikersnaam en wachtwoord op", diff --git a/public/language/nl/global.json b/public/language/nl/global.json index 94f5da88ba..446c5d35bd 100644 --- a/public/language/nl/global.json +++ b/public/language/nl/global.json @@ -50,16 +50,16 @@ "read_more": "Lees meer", "posted_ago_by_guest": "geplaatst %1 door gast", "posted_ago_by": "geplaatst %1 door %2", - "posted_ago": "geplaatst %1", - "posted_in_ago_by_guest": "geplaatst in % 1 %2 door gast", + "posted_ago": "geplaatst door %1", + "posted_in_ago_by_guest": "geplaatst in %1 %2 door gast", "posted_in_ago_by": "geplaatst in %1 %2 door %3", "posted_in_ago": "geplaatst in %1 %2", "replied_ago": "gereageerd %1", - "user_posted_ago": "%1 geplaatst %2", + "user_posted_ago": "%1 plaatste %2", "guest_posted_ago": "Gast plaatste %1", "last_edited_by_ago": "voor het laatst aangepast door %1 %2", "norecentposts": "Geen Recente Berichten", - "norecenttopics": "Geen Recente Topics", + "norecenttopics": "Geen Recente Onderwerpen", "recentposts": "Recente Berichten", "recentips": "Recente Ingelogde IPs", "away": "Afwezig", diff --git a/public/language/nl/login.json b/public/language/nl/login.json index ea7a6e3bcd..fa8f5d55d7 100644 --- a/public/language/nl/login.json +++ b/public/language/nl/login.json @@ -2,7 +2,7 @@ "username": "Gebruikersnaam / Emailadres", "remember_me": "Mij Onthouden?", "forgot_password": "Wachtwoord Vergeten?", - "alternative_logins": "Alternatieve Logins", + "alternative_logins": "Alternatieve Loginmethodes", "failed_login_attempt": "Mislukte inlog poging, probeer het later opnieuw.", "login_successful": "Je bent succesvol ingelogd!", "dont_have_account": "Heeft u nog geen account?" diff --git a/public/language/nl/modules.json b/public/language/nl/modules.json index caaae8472f..f7d98f303e 100644 --- a/public/language/nl/modules.json +++ b/public/language/nl/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Berichten Geschiedenis", "chat.pop-out": "tevoorschijn halen gesprek", "chat.maximize": "Maximaliseren", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 zegt in %2:", "composer.user_said": "%1 zegt:", "composer.discard": "Weet u het zeker dat u dit bericht niet wilt plaatsen?" diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index 63a2cc6112..0530a16f5c 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -17,7 +17,7 @@ "purge": "weggooien", "restore": "Herstellen", "move": "Verplaatsen", - "fork": "Fork", + "fork": "Afsplitsen", "banned": "verbannen", "link": "Link", "share": "Delen", @@ -36,7 +36,7 @@ "watch": "Volgen", "watch.title": "Krijg notificaties van nieuwe reacties op dit onderwerp", "share_this_post": "Deel dit Bericht", - "thread_tools.title": "Thread Gereedschap", + "thread_tools.title": "Acties", "thread_tools.markAsUnreadForAll": "Ongelezen Markeren", "thread_tools.pin": "Onderwerp Vastmaken", "thread_tools.unpin": "Onderwerp Losmaken", @@ -44,13 +44,13 @@ "thread_tools.unlock": "Onderwerp Openen", "thread_tools.move": "Onderwerp Verplaatsen", "thread_tools.move_all": "Verplaats alles", - "thread_tools.fork": "Onderwerp Forken", + "thread_tools.fork": "Onderwerp Afsplitsen", "thread_tools.delete": "Onderwerp Verwijderen", "thread_tools.delete_confirm": "Weet u het zeker dat u dit onderwerp wilt verwijderen?", "thread_tools.restore": "Onderwerp Herstellen", "thread_tools.restore_confirm": "Weet u het zeker dat u het onderwerp wilt herstellen?", - "thread_tools.purge": "Gooi onderwerp weg", - "thread_tools.purge_confirm": "Weet u het zeker dat u dit onderwerp wilt weggooien?", + "thread_tools.purge": "Wis Onderwerp ", + "thread_tools.purge_confirm": "Weet u het zeker dat u dit onderwerp wilt wissen?", "topic_move_success": "Deze onderwerp is succesvol verplaatst naar %1", "post_delete_confirm": "Weet u het zeker dat u dit bericht wilt verwijderen?", "post_restore_confirm": "Weet u het zeker dat u dit bericht wilt herstellen?", @@ -58,7 +58,7 @@ "load_categories": "Categorieën Laden", "disabled_categories_note": "Uitgeschakelde Categorieën zijn grijs", "confirm_move": "Verplaatsen", - "confirm_fork": "Fork", + "confirm_fork": "Splits", "favourite": "Favoriet", "favourites": "Favorieten", "favourites.has_no_favourites": "Je hebt geen favorieten, sla een aantal berichten op als favoriet om ze hier te zien!", @@ -67,7 +67,7 @@ "move_topics": "Verplaats onderwerpen", "move_post": "Bericht Verplaatsen", "post_moved": "Bericht verplaatst!", - "fork_topic": "Onderwerp Forken", + "fork_topic": "Afgesplitste Onderwerp ", "topic_will_be_moved_to": "Dit onderwerp zal verplaatst worden naar de categorie", "fork_topic_instruction": "Klik op de berichten die je wilt forken", "fork_no_pids": "Geen berichten geselecteerd!", @@ -84,9 +84,9 @@ "composer.thumb_file_label": "Of upload een bestand", "composer.thumb_remove": "Velden leegmaken", "composer.drag_and_drop_images": "Sleep en Zet Afbeeldingen Hier", - "more_users_and_guests": "%1 meer gebruiker(s) en %2 gast(en)", + "more_users_and_guests": "%1 of meerdere gebruiker(s) en %2 gast(en)", "more_users": "%1 meer gebruiker(s)", - "more_guests": "%1 meer gast(en)", + "more_guests": "%1 of meerdere gast(en)", "users_and_others": "%1 en %2 anderen", "sort_by": "gesorteerd op", "oldest_to_newest": "Oud naar Nieuw", diff --git a/public/language/pl/category.json b/public/language/pl/category.json index 8e6707e13d..fe8b77fe54 100644 --- a/public/language/pl/category.json +++ b/public/language/pl/category.json @@ -3,5 +3,6 @@ "no_topics": "W tej kategorii nie ma jeszcze żadnych wątków.
Dlaczego ty nie utworzysz jakiegoś?", "browsing": "przegląda", "no_replies": "Nikt jeszcze nie odpowiedział", - "share_this_category": "Udostępnij tę kategorię" + "share_this_category": "Udostępnij tę kategorię", + "ignore": "Ignoruj" } \ No newline at end of file diff --git a/public/language/pl/email.json b/public/language/pl/email.json index 9a154171c0..2498036436 100644 --- a/public/language/pl/email.json +++ b/public/language/pl/email.json @@ -14,10 +14,10 @@ "digest.cta": "Kliknij, by odwiedzić %1", "digest.unsub.info": "To podsumowanie zostało wysłane zgodnie z Twoimi ustawieniami.", "digest.daily.no_topics": "Wczoraj nie było żadnych aktywnych tematów", - "notif.chat.subject": "New chat message received from %1", - "notif.chat.cta": "Click here to continue the conversation", - "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", + "notif.chat.subject": "Nowa wiadomość chatu od %1", + "notif.chat.cta": "Kliknij tutaj, by kontynuować konwersację", + "notif.chat.unsub.info": "To powiadomienie o chacie zostało Ci wysłane zgodnie z ustawieniami Twojego konta.", "test.text1": "To jest e-mail testowy, aby sprawdzić, czy poprawnie skonfigurowałeś e-mailer w swoim NodeBB.", - "unsub.cta": "Click here to alter those settings", + "unsub.cta": "Kliknij tutaj, by zmienić te ustawienia", "closing": "Dziękujemy!" } \ No newline at end of file diff --git a/public/language/pl/error.json b/public/language/pl/error.json index c9b0a39415..19b93b1951 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -20,7 +20,7 @@ "username-too-short": "Nazwa użytkownika za krótka.", "username-too-long": "Zbyt długa nazwa użytkownika", "user-banned": "Użytkownik zbanowany", - "user-too-new": "You need to wait %1 seconds before making your first post!", + "user-too-new": "Musisz odczekać %1 sekund przed napisaniem pierwszego posta!", "no-category": "Kategoria nie istnieje.", "no-topic": "Temat nie istnieje", "no-post": "Post nie istnieje", @@ -28,7 +28,7 @@ "no-user": "Użytkownik nie istnieje", "no-teaser": "Podgląd nie istnieje", "no-privileges": "Nie masz wystarczających praw by to wykonać.", - "no-emailers-configured": "No email plugins were loaded, so a test email could not be sent", + "no-emailers-configured": "Nie zainstalowano żadnego dodatku obsługującego e-mail, więc nie można wysłać testowej wiadomości.", "category-disabled": "Kategoria wyłączona.", "topic-locked": "Temat zamknięty", "still-uploading": "Poczekaj na pełne załadowanie", @@ -59,5 +59,5 @@ "downvoting-disabled": "Ocena postów jest wyłączona", "not-enough-reputation-to-downvote": "Masz za mało reputacji by ocenić ten post.", "not-enough-reputation-to-flag": "Nie masz wystarczającej reputacji by oflagować ten post", - "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." + "reload-failed": "NodeBB napotkał problem w czasie ładowania \"%1\". Forum będzie nadal dostarczać zasoby dostępne w kliencie, jednak powinieneś cofnąć ostatnią akcję." } \ No newline at end of file diff --git a/public/language/pl/global.json b/public/language/pl/global.json index 84ac6226d5..6a4b6c40b2 100644 --- a/public/language/pl/global.json +++ b/public/language/pl/global.json @@ -18,7 +18,7 @@ "save_changes": "Zapisz zmiany", "close": "Zamknij", "pagination": "Numerowanie stron", - "pagination.out_of": "%1 poza %2", + "pagination.out_of": "%1 z %2", "pagination.enter_index": "Wpisz indeks.", "header.admin": "Administracja", "header.recent": "Ostatnie", diff --git a/public/language/pl/groups.json b/public/language/pl/groups.json index c00c111e11..ecce894b28 100644 --- a/public/language/pl/groups.json +++ b/public/language/pl/groups.json @@ -1,7 +1,7 @@ { - "view_group": "View Group", - "details.title": "Group Details", - "details.members": "Member List", - "details.has_no_posts": "This group's members have not made any posts.", - "details.latest_posts": "Latest Posts" + "view_group": "Obejrzyj grupę", + "details.title": "Szczegóły grupy", + "details.members": "Lista członków", + "details.has_no_posts": "Członkowie tej grupy nie napisali żadnych postów.", + "details.latest_posts": "Ostatnie posty" } \ No newline at end of file diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json index e63c958dc9..678e131cea 100644 --- a/public/language/pl/modules.json +++ b/public/language/pl/modules.json @@ -12,7 +12,11 @@ "chat.message-history": "Historia wiadomości", "chat.pop-out": "Pokaż czat", "chat.maximize": "Maksymalizuj", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 powiedział w %2:", "composer.user_said": "%1 powiedział:", - "composer.discard": "Na pewno chcesz ignorować ten post?" + "composer.discard": "Na pewno chcesz porzucić ten post?" } \ No newline at end of file diff --git a/public/language/pl/notifications.json b/public/language/pl/notifications.json index 493b232147..2bc2824686 100644 --- a/public/language/pl/notifications.json +++ b/public/language/pl/notifications.json @@ -2,19 +2,22 @@ "title": "Powiadomienia", "no_notifs": "Nie masz nowych powiadomień", "see_all": "Zobacz wszystkie powiadomienia", - "back_to_home": "Back to %1", + "back_to_home": "Wróć do %1", "outgoing_link": "Łącze wychodzące", - "outgoing_link_message": "You are now leaving %1.", + "outgoing_link_message": "Opuszczasz %1.", "continue_to": "Continue to %1", "return_to": "Return to %1", "new_notification": "Nowe powiadomienie", "you_have_unread_notifications": "Masz nieprzeczytane powiadomienia.", "new_message_from": "Nowa wiadomość od %1", - "upvoted_your_post": "%1
zagłosował na Twój post", - "favourited_your_post": "%1 polubił/a Twój post.", - "user_flagged_post": "%1 oznaczył/a Twój post", + "upvoted_your_post_in": "%1 zagłosował na Twój post w %2", + "moved_your_post": "%1 przesunął Twój post.", + "moved_your_topic": "%1 przesunął Twój temat.", + "favourited_your_post_in": "%1 polubił Twój post w %2.", + "user_flagged_post_in": "%1 oflagował Twój post w %2", "user_posted_to": "%1 dodał odpowiedź do %2", - "user_mentioned_you_in": "%1 mentioned you in %2", + "user_mentioned_you_in": "%1 wspomniał cię w %2", + "user_started_following_you": "%1 zaczął Cię śledzić.", "email-confirmed": "E-mail potwierdzony", "email-confirmed-message": "Dziękujemy za potwierdzenie maila. Twoje konto zostało aktywowane.", "email-confirm-error": "Wystąpił błąd.", diff --git a/public/language/pl/pages.json b/public/language/pl/pages.json index fb43e50c1a..02e459b55a 100644 --- a/public/language/pl/pages.json +++ b/public/language/pl/pages.json @@ -5,7 +5,7 @@ "recent": "Ostatnie wątki", "users": "Zarejestrowani użytkownicy", "notifications": "Powiadomienia", - "tags": "Topics tagged under \"%1\"", + "tags": "Tematy oznaczone \"%1\"", "user.edit": "Edytowanie \"%1\"", "user.following": "Obserwowani przez %1", "user.followers": "Obserwujący %1", diff --git a/public/language/pl/recent.json b/public/language/pl/recent.json index 74050bc028..207d22a1d8 100644 --- a/public/language/pl/recent.json +++ b/public/language/pl/recent.json @@ -3,6 +3,7 @@ "day": "Dzień", "week": "Tydzień", "month": "Miesiąc", - "year": "Year", + "year": "Rok", + "alltime": "Od początku", "no_recent_topics": "Brak ostatnich wątków." } \ No newline at end of file diff --git a/public/language/pl/search.json b/public/language/pl/search.json index d0ffc64f36..ed42681f15 100644 --- a/public/language/pl/search.json +++ b/public/language/pl/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 wyników pasujących do \"%2\", (%3 sekund)", + "no-matches": "Nie znaleziono postów" } \ No newline at end of file diff --git a/public/language/pl/tags.json b/public/language/pl/tags.json index f065d4bbfa..d4b35e3b39 100644 --- a/public/language/pl/tags.json +++ b/public/language/pl/tags.json @@ -1,6 +1,6 @@ { - "no_tag_topics": "There are no topics with this tag.", - "tags": "Tags", - "enter_tags_here": "Enter tags here. Press enter after each tag.", - "no_tags": "There are no tags yet." + "no_tag_topics": "Nie ma tematów z tym tagiem", + "tags": "Tagi", + "enter_tags_here": "Tutaj wpisz tagi. Naciśnij enter po każdym.", + "no_tags": "Jeszcze nie ma tagów." } \ No newline at end of file diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index 6479ca56de..08af14906e 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -14,7 +14,7 @@ "reply": "Odpowiedz", "edit": "Edytuj", "delete": "Usuń", - "purge": "Purge", + "purge": "Wyczyść", "restore": "Przywróć", "move": "Przenieś", "fork": "Skopiuj", @@ -23,7 +23,7 @@ "share": "Udostępnij", "tools": "Narzędzia", "flag": "Zgłoś", - "locked": "Locked", + "locked": "Zablokowany", "bookmark_instructions": "Kliknij tutaj, aby wrócić do ostatniej pozycji lub zamknij, aby odrzucić.", "flag_title": "Zgłoś post do moderacji", "flag_confirm": "Na pewno chcesz oznaczyć ten post?", @@ -44,17 +44,17 @@ "thread_tools.unlock": "Otwórz wątek", "thread_tools.move": "Przenieś wątek", "thread_tools.move_all": "Przenieś wszystko", - "thread_tools.fork": "Skopiuj wątek", + "thread_tools.fork": "Podziel wątek", "thread_tools.delete": "Usuń wątek", "thread_tools.delete_confirm": "Na pewno chcesz usunąć ten wątek?", "thread_tools.restore": "Przywróć wątek", "thread_tools.restore_confirm": "Na pewno chcesz przywrócić ten wątek?", - "thread_tools.purge": "Purge Topic", - "thread_tools.purge_confirm": "Are you sure you want to purge this thread?", + "thread_tools.purge": "Wyczyść wątek", + "thread_tools.purge_confirm": "Jesteś pewien, że chcesz wyczyścić ten wątek?", "topic_move_success": "Temat przeniesiono do %1", "post_delete_confirm": "Na pewno chcesz usunąć ten post?", "post_restore_confirm": "Na pewno chcesz przywrócić ten post?", - "post_purge_confirm": "Are you sure you want to purge this post?", + "post_purge_confirm": "Jesteś pewien, że chcesz wyczyścić ten post?", "load_categories": "Ładowanie kategorii", "disabled_categories_note": "Zablokowane kategorie zostały wyszarzone.", "confirm_move": "Przenieś", @@ -87,9 +87,9 @@ "more_users_and_guests": "%1 więcej użytkownik(ów) i %2 gośc(i)", "more_users": "%1 więcej użytkownik(ów)", "more_guests": "%1 więcej gośc(i)", - "users_and_others": "%1 and %2 others", - "sort_by": "Sort by", - "oldest_to_newest": "Oldest to Newest", - "newest_to_oldest": "Newest to Oldest", - "most_votes": "Most votes" + "users_and_others": "%1 i %2 innych", + "sort_by": "Sortuj po", + "oldest_to_newest": "Najpierw najstarsze", + "newest_to_oldest": "Najpierw najnowsze", + "most_votes": "Najwięcej głosów" } \ No newline at end of file diff --git a/public/language/pl/user.json b/public/language/pl/user.json index 3293cf5200..957954f1a2 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -5,7 +5,7 @@ "email": "Adres e-mail", "confirm_email": "Potwierdź e-mail", "delete_account": "Skasuj konto", - "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", + "delete_account_confirm": "Jesteś pewny, że chcesz skasować swoje konto?
Tej operacji nie można cofnąć i utracisz wszystkie swoje dane

Podaj swoją nazwę użytkownika, by potwierdzić skasowanie konta.", "fullname": "Pełna nazwa", "website": "Strona WWW", "location": "Położenie", @@ -39,20 +39,20 @@ "change_password_success": "Twoje hasło jest zaktualizowane!", "confirm_password": "Potwierdź hasło", "password": "Hasło", - "username_taken_workaround": "Login jakiego chciałeś/aś użyć jest już zajęty, ale my to załatwimy. Jesteś znany/a jako %1", + "username_taken_workaround": "Wybrany login jest już zajęty, więc zmieniliśmy go trochę. Proponujemy %1", "upload_picture": "Prześlij zdjęcie", "upload_a_picture": "Prześlij zdjęcie", "image_spec": "Możesz przesłać tylko pliki PNG, JPG lub GIF.", "max": "maks.", "settings": "Ustawienia", "show_email": "Wyświetlaj mój adres e-mail", - "digest_label": "Subskrybuj dla Digestów!", + "digest_label": "Przysyłaj okresowe podsumowanie wiadomości na forum", "digest_description": "Subskrybuj by dostawać maile z tego forum (nowe powiadomienia i tematy) zgodnie z ustalonym harmonogramem.", "digest_off": "Wyłączone", "digest_daily": "Codziennie", "digest_weekly": "Co tydzień", "digest_monthly": "Co miesiąc", - "send_chat_notifications": "Send an email if a new chat message arrives and I am not online", + "send_chat_notifications": "Wyślij e-maila, jeśli dostanę nową wiadomość z chatu, a nie jestem on-line", "has_no_follower": "Ten użytkownik nie ma jeszcze żadnych obserwujących", "follows_no_one": "Użytkownik jeszcze nikogo nie obsweruje.", "has_no_posts": "Użytkownik nie napisał jeszcze żadnych postów.", diff --git a/public/language/pl/users.json b/public/language/pl/users.json index 6a12eb61d4..1c87248f14 100644 --- a/public/language/pl/users.json +++ b/public/language/pl/users.json @@ -5,6 +5,6 @@ "search": "Szukaj", "enter_username": "Wpisz nazwę użytkownika", "load_more": "Więcej", - "user-not-found": "User not found!", - "users-found-search-took": "%1 user(s) found! Search took %2 ms." + "user-not-found": "Nie znaleziono użytkownika!", + "users-found-search-took": "Znaleziono %1 użytkowników. Szukanie zajęło %2 ms." } \ No newline at end of file diff --git a/public/language/pt_BR/category.json b/public/language/pt_BR/category.json index 7b63409f19..13a29a5a02 100644 --- a/public/language/pt_BR/category.json +++ b/public/language/pt_BR/category.json @@ -3,5 +3,6 @@ "no_topics": "Não tem nenhum tópico nesta categoria.
Por que não tenta postar o primeiro?", "browsing": "navegando", "no_replies": "Ninguém respondeu", - "share_this_category": "Compartilhar" + "share_this_category": "Compartilhar", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/pt_BR/error.json b/public/language/pt_BR/error.json index 558d87adde..d6e0da1ede 100644 --- a/public/language/pt_BR/error.json +++ b/public/language/pt_BR/error.json @@ -12,11 +12,13 @@ "invalid-title": "Título inválido.", "invalid-user-data": "Informação de usuário inválida", "invalid-password": "Senha inválida", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Informação inválida de paginação", "username-taken": "Usuário já existe", "email-taken": "Email já foi utilizado em um cadastro", "email-not-confirmed": "Seu email não está confirmado, por favor, clique aqui para confirmá-lo", "username-too-short": "Nome de usuário muito curto", + "username-too-long": "Username too long", "user-banned": "Usuário banido", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Categoria não existe", @@ -53,5 +55,9 @@ "upload-error": "Erro de Upload : %1", "signature-too-long": "Assinatura não pode conter mais que %1 caracteres!", "cant-chat-with-yourself": "Impossível bater papo consigo mesmo!", - "not-enough-reputation-to-downvote": "Você não possui reputação suficiente para negativar este post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "Você não possui reputação suficiente para negativar este post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/pt_BR/modules.json b/public/language/pt_BR/modules.json index 84f94a3de5..be9126b20d 100644 --- a/public/language/pt_BR/modules.json +++ b/public/language/pt_BR/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Histórico de Conversas", "chat.pop-out": "Minimizar o chat", "chat.maximize": "Maximizar", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 disse em %2:", "composer.user_said": "%1 disse:", "composer.discard": "Deseja mesmo remover essa postagem?" diff --git a/public/language/pt_BR/notifications.json b/public/language/pt_BR/notifications.json index 08a4240c19..fc82e93866 100644 --- a/public/language/pt_BR/notifications.json +++ b/public/language/pt_BR/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Nova notificação", "you_have_unread_notifications": "Você possui notificações não lidas.", "new_message_from": "Nova mensagem de %1", - "upvoted_your_post": "%1 votou no seu post.", - "favourited_your_post": "%1 favoritou seu post.", - "user_flagged_post": "%1 sinalizou seu post.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 respondeu para: %2", "user_mentioned_you_in": "%1 mensionou você em %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmado", "email-confirmed-message": "Obrigado por validar seu email. Sua conta foi ativada.", "email-confirm-error": "Ocorreu um erro...", diff --git a/public/language/pt_BR/pages.json b/public/language/pt_BR/pages.json index b700cccb53..48c03f52d9 100644 --- a/public/language/pt_BR/pages.json +++ b/public/language/pt_BR/pages.json @@ -5,7 +5,7 @@ "recent": "Tópicos Recentes", "users": "Usuários Registrados", "notifications": "Notificações", - "tags": "Topics tagged under \"%1\"", + "tags": "Tópicos com a tag sob \"%1\"", "user.edit": "Editando \"%1\"", "user.following": "Seguidos por %1", "user.followers": "Seguidores de %1", diff --git a/public/language/pt_BR/recent.json b/public/language/pt_BR/recent.json index 27a5e4e108..3185b1419b 100644 --- a/public/language/pt_BR/recent.json +++ b/public/language/pt_BR/recent.json @@ -4,5 +4,6 @@ "week": "Semana", "month": "Mês", "year": "Ano", + "alltime": "All Time", "no_recent_topics": "Nenhum tópico recente." } \ No newline at end of file diff --git a/public/language/pt_BR/search.json b/public/language/pt_BR/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/pt_BR/search.json +++ b/public/language/pt_BR/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/pt_BR/topic.json b/public/language/pt_BR/topic.json index 487e6e06c4..a0f0caea3d 100644 --- a/public/language/pt_BR/topic.json +++ b/public/language/pt_BR/topic.json @@ -87,7 +87,7 @@ "more_users_and_guests": "%1 mais usuário(s) e %2 visitante(s)", "more_users": "%1 mais usuário(s)", "more_guests": "%1 mais visitante(s)", - "users_and_others": "%1 and %2 others", + "users_and_others": "%1 e %2 outros", "sort_by": "Ordenar por", "oldest_to_newest": "Mais Antigo para Recente", "newest_to_oldest": "Recente para mais Antigo", diff --git a/public/language/pt_BR/user.json b/public/language/pt_BR/user.json index 8a64884889..60b2827e2a 100644 --- a/public/language/pt_BR/user.json +++ b/public/language/pt_BR/user.json @@ -4,6 +4,8 @@ "username": "Usuário", "email": "Email", "confirm_email": "Confirmar Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Nome Completo", "website": "Website", "location": "Localização", @@ -62,5 +64,7 @@ "posts_per_page": "Posts por Página", "notification_sounds": "Tocar um som quando receber notificação.", "browsing": "Configurações de Navegação", - "open_links_in_new_tab": "Abrir links externos em nova aba?" + "open_links_in_new_tab": "Abrir links externos em nova aba?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/ro/category.json b/public/language/ro/category.json index fbd7cd2393..8c5034e4fd 100644 --- a/public/language/ro/category.json +++ b/public/language/ro/category.json @@ -3,5 +3,6 @@ "no_topics": "Nu există nici un subiect de discuție în această categorie.
De ce nu încerci să postezi tu unul?", "browsing": "navighează", "no_replies": "Nu a răspuns nimeni", - "share_this_category": "Distribuie această categorie" + "share_this_category": "Distribuie această categorie", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/ro/error.json b/public/language/ro/error.json index d63d73f389..a40ac5f0d0 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -12,11 +12,13 @@ "invalid-title": "Titlu invalid!", "invalid-user-data": "Date utilizator invalide", "invalid-password": "Parolă Invalidă", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Date paginație invalide", "username-taken": "Numele de utilizator este deja folosit", "email-taken": "Adresa de email este deja folostă", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Numele de utilizator este prea scurt", + "username-too-long": "Username too long", "user-banned": "Utilizator banat", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Categoria nu există", @@ -53,5 +55,9 @@ "upload-error": "Eroare Upload : %1", "signature-too-long": "Semnătura ta nu poate fi mai lungă de %1 caractere!", "cant-chat-with-yourself": "Nu poți conversa cu tine!", - "not-enough-reputation-to-downvote": "Nu ai destulă reputație pentru a vota negativ acest post." + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "Nu ai destulă reputație pentru a vota negativ acest post.", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/ro/modules.json b/public/language/ro/modules.json index 0c4ad4d148..3310439f13 100644 --- a/public/language/ro/modules.json +++ b/public/language/ro/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Istorie Mesaje", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximizează", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 a spus în %2:", "composer.user_said": "%1 a spus:", "composer.discard": "Ești sigur că vrei să renunți la acest mesaj?" diff --git a/public/language/ro/notifications.json b/public/language/ro/notifications.json index 682cb05ee2..568af20877 100644 --- a/public/language/ro/notifications.json +++ b/public/language/ro/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Notificare Nouă", "you_have_unread_notifications": "Ai notificări necitite.", "new_message_from": "Un mesaj nou de la %1", - "upvoted_your_post": "%1 a votat pozitiv mesajul tău.", - "favourited_your_post": "%1 a adăugat mesajul tău la favorite.", - "user_flagged_post": "%1 a semnalizat un mesaj.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 a postat un răspuns la: %2", "user_mentioned_you_in": "%1 te-a menționat în %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email confirmat", "email-confirmed-message": "Îți mulțumim pentru validarea emailului. Contul tău este acuma activat.", "email-confirm-error": "S-a produs o eroare ...", diff --git a/public/language/ro/recent.json b/public/language/ro/recent.json index b17702e5e9..c0600afba9 100644 --- a/public/language/ro/recent.json +++ b/public/language/ro/recent.json @@ -4,5 +4,6 @@ "week": "Săptămână", "month": "Lună", "year": "An", + "alltime": "All Time", "no_recent_topics": "Nu există subiecte recente." } \ No newline at end of file diff --git a/public/language/ro/search.json b/public/language/ro/search.json index d0ffc64f36..7d1d5330d4 100644 --- a/public/language/ro/search.json +++ b/public/language/ro/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "Nu a fost găsit nici un mesaj!" } \ No newline at end of file diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json index 8254d0bdae..3bcdf5805e 100644 --- a/public/language/ro/topic.json +++ b/public/language/ro/topic.json @@ -54,7 +54,7 @@ "topic_move_success": "Acest mesaj a fost mutat cu succes în %1", "post_delete_confirm": "Ești sigur că vrei să ștergi acest mesaj?", "post_restore_confirm": "Esti sigur că vrei să restaurezi acest mesaj?", - "post_purge_confirm": "Are you sure you want to purge this post?", + "post_purge_confirm": "Ești sigur că vrei să cureți acest mesaj?", "load_categories": "Se Încarcă Categoriile", "disabled_categories_note": "Categoriile dezactivate sunt decolorate cu gri", "confirm_move": "Mută", @@ -87,7 +87,7 @@ "more_users_and_guests": "%1 utlizator(i) și %2 vizitator(i)", "more_users": "%1 utilizator(i)", "more_guests": "%1 vizitator(i)", - "users_and_others": "%1 and %2 others", + "users_and_others": "%1 și alți %2", "sort_by": "Sortează de la", "oldest_to_newest": "Vechi la Noi", "newest_to_oldest": "Noi la Vechi", diff --git a/public/language/ro/user.json b/public/language/ro/user.json index 16db726af5..77a53ae683 100644 --- a/public/language/ro/user.json +++ b/public/language/ro/user.json @@ -4,6 +4,8 @@ "username": "Nume utilizator", "email": "Adresă Email", "confirm_email": "Confirmă Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Nume Întreg", "website": "Pagină Web", "location": "Locație", @@ -62,5 +64,7 @@ "posts_per_page": "Mesaje pe pagină", "notification_sounds": "Redă un sunet când primești o notificare.", "browsing": "Setări navigare", - "open_links_in_new_tab": "Deschide linkurile externe intr-un tab nou?" + "open_links_in_new_tab": "Deschide linkurile externe intr-un tab nou?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/ru/category.json b/public/language/ru/category.json index 9248f1cfd9..9454af6989 100644 --- a/public/language/ru/category.json +++ b/public/language/ru/category.json @@ -3,5 +3,6 @@ "no_topics": "В этой категории еще нет тем.
Почему бы вам не создать первую?", "browsing": "просматривают", "no_replies": "Нет ответов", - "share_this_category": "Поделиться этой категорией" + "share_this_category": "Поделиться этой категорией", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/ru/error.json b/public/language/ru/error.json index 231c329733..a715f9c9a2 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -12,11 +12,13 @@ "invalid-title": "Неверный заголовок!", "invalid-user-data": "Неверные Пользовательские Данные", "invalid-password": "Неверный Пароль", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Неверное значение пагинации", "username-taken": "Имя пользователя занято", "email-taken": "Email занят", "email-not-confirmed": "Ваш email не подтвержден, нажмите для подтверждения.", "username-too-short": "Слишком короткое имя пользователя", + "username-too-long": "Username too long", "user-banned": "Пользователь заблокирован", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Несуществующая категория", @@ -53,5 +55,9 @@ "upload-error": "Ошибка загрузки : %1", "signature-too-long": "Подпись не может быть длиннее %1 символов", "cant-chat-with-yourself": "Вы не можете общаться с самим собой", - "not-enough-reputation-to-downvote": "У Вас недостаточно репутации для понижения оценки поста" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "У Вас недостаточно репутации для понижения оценки поста", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json index 5cd840d3d0..ec2079f009 100644 --- a/public/language/ru/modules.json +++ b/public/language/ru/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "История сообщений", "chat.pop-out": "Покинуть диалог", "chat.maximize": "Развернуть", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 сказал %2:", "composer.user_said": "%1 сказал:", "composer.discard": "Вы уверены, что хотите отказаться от этого поста?" diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json index 1648ebc8f8..0bd4597dd7 100644 --- a/public/language/ru/notifications.json +++ b/public/language/ru/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Новое Уведомление", "you_have_unread_notifications": "У вас есть непрочитанные уведомления", "new_message_from": "Новое сообщение от %1", - "upvoted_your_post": "%1 проголосовал за Ваш пост", - "favourited_your_post": "%1 добавил Ваш пост в избранное", - "user_flagged_post": "%1 пометил пост", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 ответил на запись: %2", "user_mentioned_you_in": "%1 упомянул вас в %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Подтвержден", "email-confirmed-message": "Спасибо за подтверждение Вашего Email-адреса. Ваш аккаунт активирован.", "email-confirm-error": "Произошла ошибка...", diff --git a/public/language/ru/recent.json b/public/language/ru/recent.json index 7dabef695e..428c2c6b3d 100644 --- a/public/language/ru/recent.json +++ b/public/language/ru/recent.json @@ -4,5 +4,6 @@ "week": "Неделя", "month": "Месяц", "year": "Год", + "alltime": "All Time", "no_recent_topics": "Нет свежих тем." } \ No newline at end of file diff --git a/public/language/ru/search.json b/public/language/ru/search.json index 5f0a069bef..17b82606bd 100644 --- a/public/language/ru/search.json +++ b/public/language/ru/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 результатов по фразе \"%2\", (%3 секунды) " + "results_matching": "%1 результатов по фразе \"%2\", (%3 секунды) ", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/ru/user.json b/public/language/ru/user.json index 654465710e..e09a5fd425 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -4,6 +4,8 @@ "username": "Имя пользователя", "email": "Email", "confirm_email": "Подтвердить Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Полное имя", "website": "Сайт", "location": "Откуда", @@ -62,5 +64,7 @@ "posts_per_page": "Постов на Странице", "notification_sounds": "Звук при получении уведомления", "browsing": "Настройки просмотра", - "open_links_in_new_tab": "Открывать ссылки, ведущие на другие сайты, в новой вкладке?" + "open_links_in_new_tab": "Открывать ссылки, ведущие на другие сайты, в новой вкладке?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/sc/category.json b/public/language/sc/category.json index d249aa6ffd..35e7f7b522 100644 --- a/public/language/sc/category.json +++ b/public/language/sc/category.json @@ -3,5 +3,6 @@ "no_topics": "Non bi sunt arresonadas in custa creze.
Pro ite non nde pones una?", "browsing": "navighende", "no_replies": "Perunu at rispostu", - "share_this_category": "Share this category" + "share_this_category": "Share this category", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/sc/error.json b/public/language/sc/error.json index 7eba6e0ff9..f90d459cce 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/sc/modules.json b/public/language/sc/modules.json index 04d1143a2c..f2ca01ab31 100644 --- a/public/language/sc/modules.json +++ b/public/language/sc/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/sc/notifications.json b/public/language/sc/notifications.json index c5c59c39e1..0fecd4d1a7 100644 --- a/public/language/sc/notifications.json +++ b/public/language/sc/notifications.json @@ -10,11 +10,14 @@ "new_notification": "New Notification", "you_have_unread_notifications": "You have unread notifications.", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/sc/recent.json b/public/language/sc/recent.json index b6d27107b3..2e2d9401c7 100644 --- a/public/language/sc/recent.json +++ b/public/language/sc/recent.json @@ -4,5 +4,6 @@ "week": "Chida", "month": "Mese", "year": "Year", + "alltime": "All Time", "no_recent_topics": "Non bi sunt ùrtimas arresonadas." } \ No newline at end of file diff --git a/public/language/sc/search.json b/public/language/sc/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/sc/search.json +++ b/public/language/sc/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/sc/user.json b/public/language/sc/user.json index f301455631..298fc5ffa0 100644 --- a/public/language/sc/user.json +++ b/public/language/sc/user.json @@ -4,6 +4,8 @@ "username": "Nùmene de Impitadore", "email": "Email", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Nùmene e Sambenadu", "website": "Giassu web", "location": "Logu", @@ -62,5 +64,7 @@ "posts_per_page": "Arresonos pro Pàgina", "notification_sounds": "Play a sound when you receive a notification.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/sk/category.json b/public/language/sk/category.json index e804178494..ead803aae6 100644 --- a/public/language/sk/category.json +++ b/public/language/sk/category.json @@ -3,5 +3,6 @@ "no_topics": "V tejto kategórií zatiaľ nie sú žiadne príspevky.
Môžeš byť prvý!", "browsing": "prehliada", "no_replies": "Nikdo ešte neodpovedal", - "share_this_category": "zdielaj túto kategóriu" + "share_this_category": "zdielaj túto kategóriu", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/sk/error.json b/public/language/sk/error.json index c3cb3a0dfa..60262ff3e4 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -12,11 +12,13 @@ "invalid-title": "Nesprávny titulok!", "invalid-user-data": "Neplatné užívatelské údaje", "invalid-password": "Nesprávne heslo", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Nesprávna hodnota stránkovania", "username-taken": "Užívateľske meno je obsadené", "email-taken": "Email je obsadený", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "Užívateľ je zakázaný", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Kategória neexistuje", @@ -53,5 +55,9 @@ "upload-error": "Upload chyba: %1", "signature-too-long": "Podpis nesmie byť dlhší ako %1 znakov!", "cant-chat-with-yourself": "Nemôžete chatovat so samým sebou.", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/sk/modules.json b/public/language/sk/modules.json index 9c661800d4..6c21458605 100644 --- a/public/language/sk/modules.json +++ b/public/language/sk/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/sk/notifications.json b/public/language/sk/notifications.json index 7a5b20cbb7..cffd1a1fc0 100644 --- a/public/language/sk/notifications.json +++ b/public/language/sk/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Nová notifikácia", "you_have_unread_notifications": "Máte neprečítané notifikácie", "new_message_from": "Nova spáva od %1", - "upvoted_your_post": "%1 zahlasoval za Váš príspevok.", - "favourited_your_post": "%1 pridal do obľubených Váš príspevok.", - "user_flagged_post": "%1 označil Váš príspevok", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 odpovedal: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email bol potvrdený", "email-confirmed-message": "Ďakujeme za potvrdenie tvojho emailu. Účet je plne aktivovaný.", "email-confirm-error": "Vyskytla sa chyba...", diff --git a/public/language/sk/recent.json b/public/language/sk/recent.json index cfb6b18bc5..963740ba54 100644 --- a/public/language/sk/recent.json +++ b/public/language/sk/recent.json @@ -4,5 +4,6 @@ "week": "Týždeň", "month": "Mesiac", "year": "Year", + "alltime": "All Time", "no_recent_topics": "Nie sú žiadne posledné témy" } \ No newline at end of file diff --git a/public/language/sk/search.json b/public/language/sk/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/sk/search.json +++ b/public/language/sk/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/sk/user.json b/public/language/sk/user.json index e63a9b45f3..2fa171f6ef 100644 --- a/public/language/sk/user.json +++ b/public/language/sk/user.json @@ -4,6 +4,8 @@ "username": "Uživateľské meno", "email": "Email", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Meno a priezvisko", "website": "Webová stránka", "location": "Poloha", @@ -62,5 +64,7 @@ "posts_per_page": "Príspevkov na stranu", "notification_sounds": "Prehraj zvuk ked príde notifikácia", "browsing": "Hľadaj v nadstaveniach", - "open_links_in_new_tab": "Otvoriť tieto odkazy v novom tabe ?" + "open_links_in_new_tab": "Otvoriť tieto odkazy v novom tabe ?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/sv/category.json b/public/language/sv/category.json index 25b9f9fa83..ff00a051b5 100644 --- a/public/language/sv/category.json +++ b/public/language/sv/category.json @@ -3,5 +3,6 @@ "no_topics": "Det finns inga ämnen i denna kategori.
Varför inte skapa ett?", "browsing": "läser", "no_replies": "Ingen har svarat", - "share_this_category": "Dela den här kategorin" + "share_this_category": "Dela den här kategorin", + "ignore": "Ignorera" } \ No newline at end of file diff --git a/public/language/sv/modules.json b/public/language/sv/modules.json index a996e30676..aea650c3a8 100644 --- a/public/language/sv/modules.json +++ b/public/language/sv/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Historik för meddelande", "chat.pop-out": "Utskjutande chatt", "chat.maximize": "Maximera", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 sa i %2:", "composer.user_said": "%1 sa:", "composer.discard": "Är du säker på att du vill förkasta det här inlägget?" diff --git a/public/language/sv/notifications.json b/public/language/sv/notifications.json index 9f262b008a..2e8e5a3f2a 100644 --- a/public/language/sv/notifications.json +++ b/public/language/sv/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Ny notis", "you_have_unread_notifications": "Du har olästa notiser.", "new_message_from": "Nytt medelande från %1", - "upvoted_your_post": "%1 har röstat på ditt inlägg.", - "favourited_your_post": "%1 har favoriserat ditt inlägg.", - "user_flagged_post": "%1 flaggade ett inlägg.", + "upvoted_your_post_in": "%1 har röstat upp ditt inlägg i %2", + "moved_your_post": "%1 har flyttat dit inlägg.", + "moved_your_topic": "%1 har flyttat ditt ämne.", + "favourited_your_post_in": "%1 har favoriserat ditt inlägg i %2.", + "user_flagged_post_in": "%1 flaggade ett inlägg i %2", "user_posted_to": "%1 har skrivit ett svar på: %2", "user_mentioned_you_in": "%1 nämnde dig i %2", + "user_started_following_you": "%1 började följa dig.", "email-confirmed": "Epost bekräftad", "email-confirmed-message": "Tack för att du bekräftat din epostadress. Ditt konto är nu fullt ut aktiverat.", "email-confirm-error": "Ett fel uppstod...", diff --git a/public/language/sv/recent.json b/public/language/sv/recent.json index 4478c47921..dc59877cf6 100644 --- a/public/language/sv/recent.json +++ b/public/language/sv/recent.json @@ -4,5 +4,6 @@ "week": "Vecka", "month": "Månad", "year": "År", + "alltime": "Alltid", "no_recent_topics": "Det finns inga olästa ämnen." } \ No newline at end of file diff --git a/public/language/sv/search.json b/public/language/sv/search.json index 996a7d81fd..49e8bde9a0 100644 --- a/public/language/sv/search.json +++ b/public/language/sv/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 resultat matchar \"%2\", (%3 sekunder)" + "results_matching": "%1 resultat matchar \"%2\", (%3 sekunder)", + "no-matches": "Inga inlägg hittades" } \ No newline at end of file diff --git a/public/language/th/category.json b/public/language/th/category.json index 790f7752fc..31246e2a05 100644 --- a/public/language/th/category.json +++ b/public/language/th/category.json @@ -3,5 +3,6 @@ "no_topics": "ยังไม่มีกระทู้ในหมวดนี้
โพสต์กระทู้แรก?", "browsing": "เรียกดู", "no_replies": "ยังไม่มีใครตอบ", - "share_this_category": "Share this category" + "share_this_category": "Share this category", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/th/error.json b/public/language/th/error.json index 7eba6e0ff9..f90d459cce 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "Invalid User Data", "invalid-password": "Invalid Password", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Invalid pagination value", "username-taken": "Username taken", "email-taken": "Email taken", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "User banned", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Category doesn't exist", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "Signature can't be longer than %1 characters!", "cant-chat-with-yourself": "You can't chat with yourself!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/th/modules.json b/public/language/th/modules.json index 90494a58ee..2f8e7734aa 100644 --- a/public/language/th/modules.json +++ b/public/language/th/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/th/notifications.json b/public/language/th/notifications.json index 4c33f78276..d78ad8e8f6 100644 --- a/public/language/th/notifications.json +++ b/public/language/th/notifications.json @@ -10,11 +10,14 @@ "new_notification": "New Notification", "you_have_unread_notifications": "You have unread notifications.", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/th/recent.json b/public/language/th/recent.json index c7a2e6bc19..05eec4d503 100644 --- a/public/language/th/recent.json +++ b/public/language/th/recent.json @@ -4,5 +4,6 @@ "week": "สัปดาห์", "month": "เดือน", "year": "Year", + "alltime": "All Time", "no_recent_topics": "ไม่มีกระทู้ล่าสุด" } \ No newline at end of file diff --git a/public/language/th/search.json b/public/language/th/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/th/search.json +++ b/public/language/th/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/th/user.json b/public/language/th/user.json index 153c1e3c2e..ba6a427e10 100644 --- a/public/language/th/user.json +++ b/public/language/th/user.json @@ -4,6 +4,8 @@ "username": "ชื่อผู้ใช้", "email": "อีเมล์", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "ชื่อเต็ม", "website": "เว็บไซต์", "location": "สถานที่", @@ -62,5 +64,7 @@ "posts_per_page": "จำนวนโพสต์ต่อหน้า", "notification_sounds": "Play a sound when you receive a notification.", "browsing": "Browsing Settings", - "open_links_in_new_tab": "Open outgoing links in new tab?" + "open_links_in_new_tab": "Open outgoing links in new tab?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/tr/category.json b/public/language/tr/category.json index 7ec2a32e14..9fae15699d 100644 --- a/public/language/tr/category.json +++ b/public/language/tr/category.json @@ -3,5 +3,6 @@ "no_topics": " Bu kategoride hiç konu yok.
Yeni bir konu açmak istemez misiniz?", "browsing": "dolaşıyor", "no_replies": "Kimse yanıtlamadı", - "share_this_category": "Bu kategoriyi paylaş" + "share_this_category": "Bu kategoriyi paylaş", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/tr/error.json b/public/language/tr/error.json index eeaee2fcd3..86b7a7567d 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -12,11 +12,13 @@ "invalid-title": "Geçersiz başlık!", "invalid-user-data": "Geçersiz Kullancı Verisi", "invalid-password": "Geçersiz Şifre", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "Geçersiz Sayfa Değeri", "username-taken": "Kullanıcı İsmi Alınmış", "email-taken": "E-posta Alınmış", "email-not-confirmed": "E-postanız onaylanmamış, onaylamak için lütfen buraya tıklayın.", "username-too-short": "Kullanıcı ismi çok kısa", + "username-too-long": "Username too long", "user-banned": "Kullanıcı Yasaklı", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "Kategori Yok", @@ -53,5 +55,9 @@ "upload-error": "Yükleme Hatası : %1", "signature-too-long": "İmza en fazla %1 karakter olabilir!", "cant-chat-with-yourself": "Kendinizle sohbet edemezsiniz!", - "not-enough-reputation-to-downvote": "Bu iletiyi aşagı oylamak için yeterince saygınlığınız yok." + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "Bu iletiyi aşagı oylamak için yeterince saygınlığınız yok.", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/tr/modules.json b/public/language/tr/modules.json index fb44fb8324..0e4d6ba82c 100644 --- a/public/language/tr/modules.json +++ b/public/language/tr/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Mesaj Geçmişi", "chat.pop-out": "Sohbeti Pencereye Çevir", "chat.maximize": "Büyüt", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 %2 içinde söyledi:", "composer.user_said": "%1 söyledi:", "composer.discard": "Bu iletiyi iptal etmek istediğinizden eminmisiniz?" diff --git a/public/language/tr/notifications.json b/public/language/tr/notifications.json index 3ff542c19a..8b5db97764 100644 --- a/public/language/tr/notifications.json +++ b/public/language/tr/notifications.json @@ -10,11 +10,14 @@ "new_notification": "Yeni bildirim", "you_have_unread_notifications": "Okunmamış bildirimleriniz var.", "new_message_from": "%1 size bir mesaj gönderdi", - "upvoted_your_post": "%1 iletinizi beğendi.", - "favourited_your_post": "%1 iletinizi favorilerine ekledi.", - "user_flagged_post": "%1 bir iletiyi sakıncalı buldu.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 %2 başlığına bir ileti gönderdi.", "user_mentioned_you_in": "%1 %2 başlığında sizden bahsetti.", + "user_started_following_you": "%1 started following you.", "email-confirmed": "E-posta onaylandı", "email-confirmed-message": "E-postanızı onaylandığınız için teşekkürler. Hesabınız tamamen aktive edildi.", "email-confirm-error": "Bir hata oluştu...", diff --git a/public/language/tr/recent.json b/public/language/tr/recent.json index 333a85f772..088eacdff1 100644 --- a/public/language/tr/recent.json +++ b/public/language/tr/recent.json @@ -4,5 +4,6 @@ "week": "Hafta", "month": "Ay", "year": "Yıl", + "alltime": "All Time", "no_recent_topics": "Güncel konular yok." } \ No newline at end of file diff --git a/public/language/tr/search.json b/public/language/tr/search.json index b384c82c72..307b7cffb3 100644 --- a/public/language/tr/search.json +++ b/public/language/tr/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 tane “%2“ bulundu (%3 saniye)" + "results_matching": "%1 tane “%2“ bulundu (%3 saniye)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/tr/user.json b/public/language/tr/user.json index 2678ba7827..8d9584f0bc 100644 --- a/public/language/tr/user.json +++ b/public/language/tr/user.json @@ -4,6 +4,8 @@ "username": "Kullanıcı Adı", "email": "E-posta", "confirm_email": "E-posta onayla", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "Tam Ad", "website": "Websitesi", "location": "Konum", @@ -62,5 +64,7 @@ "posts_per_page": "Sayfa başına İletiler", "notification_sounds": "Bildirim alındığında ses çal", "browsing": "Tarayıcı Ayaları", - "open_links_in_new_tab": "Dışarı giden bağlantıları yeni sekmede aç?" + "open_links_in_new_tab": "Dışarı giden bağlantıları yeni sekmede aç?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/language/vi/category.json b/public/language/vi/category.json index 972df0d4f3..a1074bb468 100644 --- a/public/language/vi/category.json +++ b/public/language/vi/category.json @@ -3,5 +3,6 @@ "no_topics": "Không có bài viết trong danh mục.
Hãy đăng một bài viết mới?", "browsing": "đang duyệt", "no_replies": "Chưa có ai bình luận", - "share_this_category": "Chia sẻ phần mục này" + "share_this_category": "Chia sẻ phần mục này", + "ignore": "Bỏ qua" } \ No newline at end of file diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index 6afe9ffa9c..f0c69eef1c 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Lịch sử tin nhắn", "chat.pop-out": "Bật cửa sổ chat", "chat.maximize": "Phóng to", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 đã nói trong %2:", "composer.user_said": "%1 đã nói:", "composer.discard": "Bạn có chắc chắn hủy bỏ bài viết này?" diff --git a/public/language/vi/notifications.json b/public/language/vi/notifications.json index 431305ad23..48f6174299 100644 --- a/public/language/vi/notifications.json +++ b/public/language/vi/notifications.json @@ -2,22 +2,25 @@ "title": "Thông báo", "no_notifs": "Bạn không có thông báo nào mới", "see_all": "Xem tất cả thông báo", - "back_to_home": "Back to %1", + "back_to_home": "Quay lại %1", "outgoing_link": "Liên kết ngoài", - "outgoing_link_message": "You are now leaving %1.", - "continue_to": "Continue to %1", - "return_to": "Return to %1", + "outgoing_link_message": "Bạn đang rời khỏi %1.", + "continue_to": "Tiếp tục tới %1", + "return_to": "Quay lại %1", "new_notification": "Thông báo mới", "you_have_unread_notifications": "Bạn có thông báo chưa đọc", "new_message_from": "Tin nhắn mới từ %1", - "upvoted_your_post": "%1 đã hủy vote cho bài viết của bạn", - "favourited_your_post": "%1 thích bài viết của bạn", - "user_flagged_post": "%1 đã flag một bài viết", + "upvoted_your_post_in": "%1 đã bình chọn bài của bạn trong %2.", + "moved_your_post": "%1 đã di chuyển bài của bạn.", + "moved_your_topic": "%1 đã di chuyển chủ đề của bạn.", + "favourited_your_post_in": "%1 đã thích bài của bạn trong %2.", + "user_flagged_post_in": "%1 gắn cờ 1 bài trong %2", "user_posted_to": "%1 đã trả lời %2", - "user_mentioned_you_in": "%1 mentioned you in %2", - "email-confirmed": "Email Confirmed", - "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", - "email-confirm-error": "An error occurred...", - "email-confirm-error-message": "There was a problem validating your email address. Perhaps the code was invalid or has expired.", - "email-confirm-sent": "Confirmation email sent." + "user_mentioned_you_in": "%1 nhắc đến bạn trong %2", + "user_started_following_you": "%1 đã theo dõi bạn.", + "email-confirmed": "Đã xác nhận email", + "email-confirmed-message": "Cảm ơn bạn đã xác nhận địa chỉ email của bạn. Tài khoản của bạn đã được kích hoạt đầy đủ.", + "email-confirm-error": "Đã có lỗi xảy ra...", + "email-confirm-error-message": "Đã có lỗi khi xác nhận địa chỉ email. Có thể đoạn mã không đúng hoặc đã hết hạn.", + "email-confirm-sent": "Email xác nhận đã gửi." } \ No newline at end of file diff --git a/public/language/vi/recent.json b/public/language/vi/recent.json index 907cf76923..32dc489353 100644 --- a/public/language/vi/recent.json +++ b/public/language/vi/recent.json @@ -3,6 +3,7 @@ "day": "Ngày", "week": "Tuần", "month": "Tháng", - "year": "Year", + "year": "Năm", + "alltime": "Tất cả thời gian", "no_recent_topics": "Không có chủ đề nào gần đây" } \ No newline at end of file diff --git a/public/language/vi/search.json b/public/language/vi/search.json index d0ffc64f36..323dea6a5e 100644 --- a/public/language/vi/search.json +++ b/public/language/vi/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 kết quả(s) trùng với \"%2\", (%3 giây)", + "no-matches": "Không tìm thấy bài viết nào" } \ No newline at end of file diff --git a/public/language/zh_CN/modules.json b/public/language/zh_CN/modules.json index ec5334cdc8..e4a7c146f6 100644 --- a/public/language/zh_CN/modules.json +++ b/public/language/zh_CN/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "消息历史", "chat.pop-out": "弹出聊天窗口", "chat.maximize": "最大化", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 在 %2 中说:", "composer.user_said": "%1 说:", "composer.discard": "确定想要取消此帖?" diff --git a/public/language/zh_CN/pages.json b/public/language/zh_CN/pages.json index 32c3ba5e56..28af9ea173 100644 --- a/public/language/zh_CN/pages.json +++ b/public/language/zh_CN/pages.json @@ -5,7 +5,7 @@ "recent": "最新主题", "users": "已注册用户", "notifications": "提醒", - "tags": "Topics tagged under \"%1\"", + "tags": "话题为 \"%1\" 的主题", "user.edit": "正在编辑 \"%1\"", "user.following": "%1 关注", "user.followers": "关注 %1 的人", diff --git a/public/language/zh_CN/topic.json b/public/language/zh_CN/topic.json index 125c6818c0..098584ec36 100644 --- a/public/language/zh_CN/topic.json +++ b/public/language/zh_CN/topic.json @@ -87,7 +87,7 @@ "more_users_and_guests": "%1 名会员和 %2 名游客", "more_users": "%1 名会员", "more_guests": "%1 名游客", - "users_and_others": "%1 and %2 others", + "users_and_others": "%1 和 %2 其他人", "sort_by": "排序", "oldest_to_newest": "从旧到新", "newest_to_oldest": "从新到旧", diff --git a/public/language/zh_TW/category.json b/public/language/zh_TW/category.json index f1e867bb47..3fb8b883ee 100644 --- a/public/language/zh_TW/category.json +++ b/public/language/zh_TW/category.json @@ -3,5 +3,6 @@ "no_topics": "這個版面還沒有任何內容。
趕緊來發文章吧!", "browsing": "正在瀏覽", "no_replies": "還沒有回覆", - "share_this_category": "分享這類別" + "share_this_category": "分享這類別", + "ignore": "Ignore" } \ No newline at end of file diff --git a/public/language/zh_TW/error.json b/public/language/zh_TW/error.json index 2d16d5ffdb..544d11f42e 100644 --- a/public/language/zh_TW/error.json +++ b/public/language/zh_TW/error.json @@ -12,11 +12,13 @@ "invalid-title": "Invalid title!", "invalid-user-data": "無效的使用者資料", "invalid-password": "無效的密碼", + "invalid-username-or-password": "Please specify both a username and password", "invalid-pagination-value": "無效的分頁數值", "username-taken": "該使用者名稱已被使用", "email-taken": "該信箱已被使用", "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", "username-too-short": "Username too short", + "username-too-long": "Username too long", "user-banned": "該使用者已被停用", "user-too-new": "You need to wait %1 seconds before making your first post!", "no-category": "類別並不存在", @@ -53,5 +55,9 @@ "upload-error": "Upload Error : %1", "signature-too-long": "簽名檔長度不能超過 %1 字元!", "cant-chat-with-yourself": "你不能與自己聊天!", - "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post" + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", + "not-enough-reputation-to-flag": "Yo do not have enough reputation to flag this post", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading." } \ No newline at end of file diff --git a/public/language/zh_TW/modules.json b/public/language/zh_TW/modules.json index 7efe5fbdd2..f5a71c026a 100644 --- a/public/language/zh_TW/modules.json +++ b/public/language/zh_TW/modules.json @@ -12,6 +12,10 @@ "chat.message-history": "Message History", "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", + "chat.yesterday": "Yesterday", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", "composer.user_said_in": "%1 said in %2:", "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" diff --git a/public/language/zh_TW/notifications.json b/public/language/zh_TW/notifications.json index 56c9534641..e9132a07df 100644 --- a/public/language/zh_TW/notifications.json +++ b/public/language/zh_TW/notifications.json @@ -10,11 +10,14 @@ "new_notification": "新訊息通知", "you_have_unread_notifications": "您有未讀的訊息!", "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.", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "moved_your_post": "%1 has moved your post.", + "moved_your_topic": "%1 has moved your topic.", + "favourited_your_post_in": "%1 has favourited your post in %2.", + "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to": "%1 has posted a reply to: %2", "user_mentioned_you_in": "%1 mentioned you in %2", + "user_started_following_you": "%1 started following you.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", "email-confirm-error": "An error occurred...", diff --git a/public/language/zh_TW/recent.json b/public/language/zh_TW/recent.json index f264357969..1b53b1220c 100644 --- a/public/language/zh_TW/recent.json +++ b/public/language/zh_TW/recent.json @@ -4,5 +4,6 @@ "week": "本周", "month": "本月", "year": "Year", + "alltime": "All Time", "no_recent_topics": "最近沒新主題." } \ No newline at end of file diff --git a/public/language/zh_TW/search.json b/public/language/zh_TW/search.json index d0ffc64f36..e9eef4632a 100644 --- a/public/language/zh_TW/search.json +++ b/public/language/zh_TW/search.json @@ -1,3 +1,4 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No posts found" } \ No newline at end of file diff --git a/public/language/zh_TW/user.json b/public/language/zh_TW/user.json index eddb74ac5f..46def28d26 100644 --- a/public/language/zh_TW/user.json +++ b/public/language/zh_TW/user.json @@ -4,6 +4,8 @@ "username": "使用者名稱", "email": "Email", "confirm_email": "Confirm Email", + "delete_account": "Delete Account", + "delete_account_confirm": "Are you sure you want to delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your username to confirm that you wish to destroy this account.", "fullname": "姓名", "website": "網站", "location": "地址", @@ -62,5 +64,7 @@ "posts_per_page": "每頁的文章數", "notification_sounds": "當收到新消息時播放提示音", "browsing": "瀏覽設定", - "open_links_in_new_tab": "在新的分頁開啟外部連結?" + "open_links_in_new_tab": "在新的分頁開啟外部連結?", + "follow_topics_you_reply_to": "Follow topics that you reply to.", + "follow_topics_you_create": "Follow topics you create." } \ No newline at end of file diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index 06e5731983..7fd621a3ff 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -2,339 +2,310 @@ var ajaxify = ajaxify || {}; -(function () { - /*global app, templates, utils, socket, translator, config, RELATIVE_PATH*/ - - var location = document.location || window.location, - rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''), - templatesConfig = null, - availableTemplates = null, - apiXHR = null, - - PRELOADER_RATE_LIMIT = 10000; - - window.onpopstate = function (event) { - if (event !== null && event.state && event.state.url !== undefined && !ajaxify.initialLoad) { - ajaxify.go(event.state.url, function() { - $(window).trigger('action:popstate', {url: event.state.url}); - }, true); - } - }; - - ajaxify.currentPage = null; - ajaxify.initialLoad = false; - ajaxify.preloader = {}; - - function onAjaxError(err, url) { - var data = err.data, textStatus = err.textStatus; - - $('#content, #footer').removeClass('ajaxifying'); - - if (data) { - if (data.status === 404) { - return ajaxify.go('404'); - } else if (data.status === 403) { - app.alertError('[[global:please_log_in]]'); - app.previousUrl = url; - return ajaxify.go('login'); - } else if (data.status === 302) { - return ajaxify.go(data.responseJSON.slice(1)); +$(document).ready(function() { + require(['templates'], function (templatesModule) { + /*global app, templates, utils, socket, translator, config, RELATIVE_PATH*/ + + var location = document.location || window.location, + rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''), + apiXHR = null, + + PRELOADER_RATE_LIMIT = 10000; + + window.onpopstate = function (event) { + if (event !== null && event.state && event.state.url !== undefined && !ajaxify.initialLoad) { + ajaxify.go(event.state.url, function() { + $(window).trigger('action:popstate', {url: event.state.url}); + }, true); + } + }; + + ajaxify.currentPage = null; + ajaxify.initialLoad = false; + ajaxify.preloader = {}; + + function onAjaxError(err, url, callback, quiet) { + var data = err.data, textStatus = err.textStatus; + + $('#content, #footer').removeClass('ajaxifying'); + + if (data) { + if (data.status === 404) { + return ajaxify.go('404'); + } else if (data.status === 403) { + app.alertError('[[global:please_log_in]]'); + app.previousUrl = url; + return ajaxify.go('login'); + } else if (data.status === 302) { + return ajaxify.go(data.responseJSON.slice(1), callback, quiet); + } + } else if (textStatus !== "abort") { + app.alertError(data.responseJSON.error); } - } else if (textStatus !== "abort") { - app.alertError(data.responseJSON.error); } - } - ajaxify.go = function (url, callback, quiet) { - // "quiet": If set to true, will not call pushState - app.enterRoom('global'); + ajaxify.go = function (url, callback, quiet) { + // "quiet": If set to true, will not call pushState + app.enterRoom(''); - $(window).off('scroll'); + $(window).off('scroll'); - $(window).trigger('action:ajaxify.start', {url: url}); + $(window).trigger('action:ajaxify.start', {url: url}); - if ($('#content').hasClass('ajaxifying') && apiXHR) { - apiXHR.abort(); - } + if ($('#content').hasClass('ajaxifying') && apiXHR) { + apiXHR.abort(); + } - // Remove trailing slash - url = url.replace(/\/$/, ""); + // Remove trailing slash + url = url.replace(/\/$/, ""); - url = ajaxify.removeRelativePath(url); + url = ajaxify.removeRelativePath(url); - var tpl_url = ajaxify.getTemplateMapping(url); + var tpl_url = ajaxify.getTemplateMapping(url); - var hash = ''; - if(ajaxify.initialLoad) { - hash = window.location.hash ? window.location.hash : ''; - } + var hash = ''; + if(ajaxify.initialLoad) { + hash = window.location.hash ? window.location.hash : ''; + } - if (ajaxify.isTemplateAvailable(tpl_url) && !!!templatesConfig.force_refresh[tpl_url]) { - ajaxify.currentPage = url; + if (ajaxify.isTemplateAvailable(tpl_url) && !!!templatesModule.config.force_refresh[tpl_url]) { + ajaxify.currentPage = url; - if (window.history && window.history.pushState) { - window.history[!quiet ? 'pushState' : 'replaceState']({ - url: url + hash - }, url, RELATIVE_PATH + '/' + url + hash); - } + if (window.history && window.history.pushState) { + window.history[!quiet ? 'pushState' : 'replaceState']({ + url: url + hash + }, url, RELATIVE_PATH + '/' + url + hash); + } - translator.load(config.defaultLang, tpl_url); + translator.load(config.defaultLang, tpl_url); - $('#footer, #content').removeClass('hide').addClass('ajaxifying'); - var animationDuration = parseFloat($('#content').css('transition-duration')) || 0.2, - startTime = (new Date()).getTime(); + $('#footer, #content').removeClass('hide').addClass('ajaxifying'); + var animationDuration = parseFloat($('#content').css('transition-duration')) || 0.2, + startTime = (new Date()).getTime(); - ajaxify.variables.flush(); - ajaxify.loadData(url, function(err, data) { - if (err) { - return onAjaxError(err, url); - } + ajaxify.variables.flush(); + ajaxify.loadData(url, function(err, data) { + if (err) { + return onAjaxError(err, url, callback, quiet); + } - $(window).trigger('action:ajaxify.loadingTemplates', {}); + $(window).trigger('action:ajaxify.loadingTemplates', {}); - templates.parse(tpl_url, data, function(template) { - translator.translate(template, function(translatedTemplate) { - setTimeout(function() { - $('#content').html(translatedTemplate); + templates.parse(tpl_url, data, function(template) { + translator.translate(template, function(translatedTemplate) { + setTimeout(function() { + $('#content').html(translatedTemplate); - ajaxify.variables.parse(); + ajaxify.variables.parse(); - ajaxify.widgets.render(tpl_url, url, function() { - $(window).trigger('action:ajaxify.end', {url: url}); - }); + ajaxify.widgets.render(tpl_url, url, function() { + $(window).trigger('action:ajaxify.end', {url: url}); + }); - $(window).trigger('action:ajaxify.contentLoaded', {url: url}); + $(window).trigger('action:ajaxify.contentLoaded', {url: url}); - ajaxify.loadScript(tpl_url); + ajaxify.loadScript(tpl_url); - if (typeof callback === 'function') { - callback(); - } + if (typeof callback === 'function') { + callback(); + } + + app.processPage(); - app.processPage(); + $('#content, #footer').removeClass('ajaxifying'); + ajaxify.initialLoad = false; - $('#content, #footer').removeClass('ajaxifying'); - ajaxify.initialLoad = false; + app.refreshTitle(url); + }, animationDuration * 1000 - ((new Date()).getTime() - startTime)); - app.refreshTitle(url); - }, animationDuration * 1000 - ((new Date()).getTime() - startTime)) + }); + }); + require(['search'], function(search) { + search.topicDOM.end(); }); }); - }); - return true; - } + return true; + } - return false; - }; + return false; + }; - ajaxify.removeRelativePath = function(url) { - if (url.indexOf(RELATIVE_PATH.slice(1)) === 0) { - url = url.slice(RELATIVE_PATH.length); - } - return url; - }; + 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); - }; + ajaxify.refresh = function() { + ajaxify.go(ajaxify.currentPage); + }; - ajaxify.loadScript = function(tpl_url, callback) { - require(['forum/' + tpl_url], function(script) { - if (script && script.init) { - script.init(); - } + ajaxify.loadScript = function(tpl_url, callback) { + require(['forum/' + tpl_url], function(script) { + if (script && script.init) { + script.init(); + } - if (callback) { - callback(); - } - }); - }; + if (callback) { + callback(); + } + }); + }; - ajaxify.isTemplateAvailable = function(tpl) { - return $.inArray(tpl + '.tpl', availableTemplates) !== -1; - }; + ajaxify.isTemplateAvailable = function(tpl) { + return $.inArray(tpl + '.tpl', templatesModule.available) !== -1; + }; - ajaxify.getTemplateMapping = function(url) { - var tpl_url = ajaxify.getCustomTemplateMapping(url.split('?')[0]); + ajaxify.getTemplateMapping = function(url) { + var tpl_url = ajaxify.getCustomTemplateMapping(url.split('?')[0]); - if (tpl_url === false && !templates[url]) { - tpl_url = url.split('/'); + if (tpl_url === false && !templates[url]) { + tpl_url = url.split('/'); - while(tpl_url.length) { - if (ajaxify.isTemplateAvailable(tpl_url.join('/'))) { - tpl_url = tpl_url.join('/'); - break; + while(tpl_url.length) { + if (ajaxify.isTemplateAvailable(tpl_url.join('/'))) { + tpl_url = tpl_url.join('/'); + break; + } + tpl_url.pop(); } - tpl_url.pop(); - } - if (!tpl_url.length) { - tpl_url = url.split('/')[0].split('?')[0]; + if (!tpl_url.length) { + tpl_url = url.split('/')[0].split('?')[0]; + } + } else if (templates[url]) { + tpl_url = url; } - } else if (templates[url]) { - tpl_url = url; - } - return tpl_url; - }; + return tpl_url; + }; - ajaxify.getCustomTemplateMapping = function(tpl) { - if (templatesConfig && templatesConfig.custom_mapping && tpl !== undefined) { - for (var pattern in templatesConfig.custom_mapping) { - if (tpl.match(pattern)) { - return (templatesConfig.custom_mapping[pattern]); + ajaxify.getCustomTemplateMapping = function(tpl) { + if (templatesModule.config && templatesModule.config.custom_mapping && tpl !== undefined) { + for (var pattern in templatesModule.config.custom_mapping) { + if (tpl.match(pattern)) { + return (templatesModule.config.custom_mapping[pattern]); + } } } - } - return false; - }; + return false; + }; - ajaxify.loadData = function(url, callback) { - url = ajaxify.removeRelativePath(url); + ajaxify.loadData = function(url, callback) { + url = ajaxify.removeRelativePath(url); - $(window).trigger('action:ajaxify.loadingData', {url: url}); + $(window).trigger('action:ajaxify.loadingData', {url: url}); - if (ajaxify.preloader && ajaxify.preloader[url] && !ajaxify.preloader[url].loading) { - callback(null, ajaxify.preloader[url].data); - ajaxify.preloader = {}; - return; - } + if (ajaxify.preloader && ajaxify.preloader[url] && !ajaxify.preloader[url].loading) { + callback(null, ajaxify.preloader[url].data); + ajaxify.preloader = {}; + return; + } - var location = document.location || window.location, - tpl_url = ajaxify.getCustomTemplateMapping(url.split('?')[0]); + var location = document.location || window.location, + tpl_url = ajaxify.getCustomTemplateMapping(url.split('?')[0]); - if (!tpl_url) { - tpl_url = ajaxify.getTemplateMapping(url); - } + if (!tpl_url) { + tpl_url = ajaxify.getTemplateMapping(url); + } - apiXHR = $.ajax({ - url: RELATIVE_PATH + '/api/' + url, - cache: false, - success: function(data) { - if (!data) { - ajaxify.go('404'); - return; - } + apiXHR = $.ajax({ + url: RELATIVE_PATH + '/api/' + url, + cache: false, + success: function(data) { + if (!data) { + ajaxify.go('404'); + return; + } - data.relative_path = RELATIVE_PATH; + data.relative_path = RELATIVE_PATH; - if (callback) { - callback(null, data); - } - }, - error: function(data, textStatus) { - callback({ - data: data, - textStatus: textStatus - }); - } - }); - }; - - ajaxify.loadTemplate = function(template, callback) { - if (templates.cache[template]) { - callback(templates.cache[template]); - } else { - $.ajax({ - url: RELATIVE_PATH + '/templates/' + template + '.tpl' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), - type: 'GET', - success: function(data) { - callback(data.toString()); + if (callback) { + callback(null, data); + } }, - error: function(error) { - throw new Error("Unable to load template: " + template + " (" + error.statusText + ")"); + error: function(data, textStatus) { + callback({ + data: data, + textStatus: textStatus + }); } }); - } - }; - - $('document').ready(function () { - if (!window.history || !window.history.pushState) { - return; // no ajaxification for old browsers - } - - function hrefEmpty(href) { - return href === undefined || href === '' || href === 'javascript:;' || href === window.location.href + "#" || href.slice(0, 1) === "#"; - } + }; + + ajaxify.loadTemplate = function(template, callback) { + if (templates.cache[template]) { + callback(templates.cache[template]); + } else { + $.ajax({ + url: RELATIVE_PATH + '/templates/' + template + '.tpl' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), + type: 'GET', + success: function(data) { + callback(data.toString()); + }, + error: function(error) { + throw new Error("Unable to load template: " + template + " (" + error.statusText + ")"); + } + }); + } + }; - // Enhancing all anchors to ajaxify... - $(document.body).on('click', 'a', function (e) { - if (hrefEmpty(this.href) || this.target !== '' || this.protocol === 'javascript:' || $(this).attr('data-ajaxify') === 'false') { - return; + $('document').ready(function () { + if (!window.history || !window.history.pushState) { + return; // no ajaxification for old browsers } - if(!window.location.pathname.match(/\/(403|404)$/g)) { - app.previousUrl = window.location.href; + function hrefEmpty(href) { + return href === undefined || href === '' || href === 'javascript:;' || href === window.location.href + "#" || href.slice(0, 1) === "#"; } - if ((!e.ctrlKey && !e.shiftKey && !e.metaKey) && e.which === 1) { - if (this.host === window.location.host) { - // Internal link - var url = this.href.replace(rootUrl + '/', ''); + // Enhancing all anchors to ajaxify... + $(document.body).on('click', 'a', function (e) { + if (hrefEmpty(this.href) || this.target !== '' || this.protocol === 'javascript:' || $(this).attr('data-ajaxify') === 'false') { + return; + } - if(window.location.pathname === this.pathname && this.hash) { - if (this.hash !== window.location.hash) { - window.location.hash = this.hash; - } + if(!window.location.pathname.match(/\/(403|404)$/g)) { + app.previousUrl = window.location.href; + } - ajaxify.loadScript(ajaxify.getTemplateMapping(url)); - e.preventDefault(); - } else { - if (ajaxify.go(url)) { + if ((!e.ctrlKey && !e.shiftKey && !e.metaKey) && e.which === 1) { + if (this.host === window.location.host) { + // Internal link + var url = this.href.replace(rootUrl + '/', ''); + + if(window.location.pathname === this.pathname && this.hash) { + if (this.hash !== window.location.hash) { + window.location.hash = this.hash; + } + + ajaxify.loadScript(ajaxify.getTemplateMapping(url)); + e.preventDefault(); + } else { + if (ajaxify.go(url)) { + e.preventDefault(); + } + } + } else if (window.location.pathname !== '/outgoing') { + // External Link + if (config.openOutgoingLinksInNewTab) { + window.open(this.href, '_blank'); + e.preventDefault(); + } else if (config.useOutgoingLinksPage) { + ajaxify.go('outgoing?url=' + encodeURIComponent(this.href)); e.preventDefault(); } } - } else if (window.location.pathname !== '/outgoing') { - // External Link - if (config.openOutgoingLinksInNewTab) { - window.open(this.href, '_blank'); - e.preventDefault(); - } else if (config.useOutgoingLinksPage) { - ajaxify.go('outgoing?url=' + encodeURIComponent(this.href)); - e.preventDefault(); - } - } - } - }); - - $(document.body).on('mouseover', 'a', function (e) { - if (hrefEmpty(this.href) || this.target !== '' || this.protocol === 'javascript:' || $(this).attr('data-ajaxify') === 'false') { - return; - } - - if (this.host === window.location.host) { - // Internal link - var url = this.href.replace(rootUrl + '/', ''), - currentTime = (new Date()).getTime(); - - if (!ajaxify.preloader[url] || (!ajaxify.preloader[url].loading && currentTime - ajaxify.preloader[url].lastFetched > PRELOADER_RATE_LIMIT)) { - ajaxify.preloader[url] = { - loading: true - }; - ajaxify.loadData(url, function(err, data) { - ajaxify.preloader[url] = err ? null : { - url: url, - data: data, - lastFetched: currentTime, - loading: false - }; - }); } - } + }); + templates.registerLoader(ajaxify.loadTemplate); + templatesModule.refresh(app.load); }); - templates.registerLoader(ajaxify.loadTemplate); - - $.getJSON(RELATIVE_PATH + '/api/get_templates_listing', function (data) { - templatesConfig = data.templatesConfig; - availableTemplates = data.availableTemplates; - - app.load(); - }); }); - -}()); +}); \ No newline at end of file diff --git a/public/src/app.js b/public/src/app.js index 64e5e3e15c..8138f956f2 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -45,32 +45,15 @@ var socket, case 'admin': room = 'admin'; break; - default: - room = 'global'; + case 'home': + room = 'home'; break; } - - app.enterRoom(room, true); + app.currentRoom = ''; + app.enterRoom(room); socket.emit('meta.reconnected'); - socket.removeAllListeners('event:nodebb.ready'); - socket.on('event:nodebb.ready', function(cacheBuster) { - if (app.cacheBuster !== cacheBuster) { - app.cacheBuster = cacheBuster; - - app.alert({ - alert_id: 'forum_updated', - title: '[[global:updated.title]]', - message: '[[global:updated.message]]', - clickfn: function() { - window.location.reload(); - }, - type: 'warning' - }); - } - }); - $(window).trigger('action:reconnected'); setTimeout(function() { @@ -106,6 +89,7 @@ var socket, socket.on('event:connect', function (data) { app.username = data.username; app.userslug = data.userslug; + app.picture = data.picture; app.uid = data.uid; app.isAdmin = data.isAdmin; @@ -159,12 +143,10 @@ var socket, }, 1000); }); - app.enterRoom('global'); - app.cacheBuster = config['cache-buster']; bootbox.setDefaults({ - locale: config.defaultLang + locale: config.userLang }); } } @@ -178,9 +160,7 @@ var socket, }; app.logout = function() { - $.post(RELATIVE_PATH + '/logout', { - _csrf: $('#csrf_token').val() - }, function() { + $.post(RELATIVE_PATH + '/logout', function() { window.location.href = RELATIVE_PATH + '/'; }); }; @@ -215,15 +195,18 @@ var socket, }); }; - app.enterRoom = function (room, force) { + app.enterRoom = function (room) { if (socket) { - if (app.currentRoom === room && !force) { + if (app.currentRoom === room) { return; } socket.emit('meta.rooms.enter', { - 'enter': room, - 'leave': app.currentRoom + enter: room, + leave: app.currentRoom, + username: app.username, + userslug: app.userslug, + picture: app.picture }); app.currentRoom = room; @@ -392,11 +375,12 @@ var socket, }); }; - function updateOnlineStatus(uid) { - socket.emit('user.isOnline', uid, function(err, data) { - $('#logged-in-menu #user_label #user-profile-link>i').attr('class', 'fa fa-circle status ' + data.status); - }); - } + app.toggleNavbar = function(state) { + var navbarEl = $('.navbar'); + if (navbarEl) { + navbarEl.toggleClass('hidden', !!!state); + } + }; function exposeConfigToTemplates() { $(document).ready(function() { @@ -436,11 +420,20 @@ var socket, searchFields = $("#search-fields"), searchInput = $('#search-fields input'); + $('#search-form').on('submit', dismissSearch); + searchInput.on('blur', dismissSearch); + function dismissSearch(){ searchFields.hide(); searchButton.show(); } + function prepareSearch() { + searchFields.removeClass('hide').show(); + searchButton.hide(); + searchInput.focus(); + } + searchButton.on('click', function(e) { if (!config.loggedIn && !config.allowGuestSearching) { app.alert({ @@ -452,21 +445,41 @@ var socket, } e.stopPropagation(); - searchFields.removeClass('hide').show(); - $(this).hide(); - - searchInput.focus(); - - $('#search-form').on('submit', dismissSearch); - searchInput.on('blur', dismissSearch); + prepareSearch(); return false; }); - $('#search-form').on('submit', function () { - var input = $(this).find('input'); - ajaxify.go('search/' + input.val().replace(/^[ ?#]*/, '')); - input.val(''); - return false; + require(['search', 'mousetrap'], function(search, Mousetrap) { + $('#search-form').on('submit', function (e) { + e.preventDefault(); + var input = $(this).find('input'), + term = input.val(); + + + search.query(term, function() { + input.val(''); + }); + }); + + $('.topic-search') + .on('click', '.prev', function() { + search.topicDOM.prev(); + }) + .on('click', '.next', function() { + search.topicDOM.next(); + }); + + Mousetrap.bind('ctrl+f', function(e) { + // If in topic, open search window and populate, otherwise regular behaviour + var match = ajaxify.currentPage.match(/^topic\/([\d]+)/), + tid; + if (match) { + e.preventDefault(); + tid = match[1]; + searchInput.val('in:topic-' + tid + ' '); + prepareSearch(); + } + }); }); } @@ -480,11 +493,12 @@ var socket, function handleStatusChange() { $('#user-control-list .user-status').off('click').on('click', function(e) { - socket.emit('user.setStatus', $(this).attr('data-status'), function(err, data) { + var status = $(this).attr('data-status'); + socket.emit('user.setStatus', status, function(err, data) { if(err) { return app.alertError(err.message); } - updateOnlineStatus(data.uid); + $('#logged-in-menu #user_label #user-profile-link>i').attr('class', 'fa fa-circle status ' + status); }); e.preventDefault(); }); @@ -542,6 +556,33 @@ var socket, }); }); }); + + socket.removeAllListeners('event:nodebb.ready'); + socket.on('event:nodebb.ready', function(cacheBusters) { + if ( + !app.cacheBusters || + app.cacheBusters.general !== cacheBusters.general || + app.cacheBusters.css !== cacheBusters.css || + app.cacheBusters.js !== cacheBusters.js + ) { + app.cacheBusters = cacheBusters; + + app.alert({ + alert_id: 'forum_updated', + title: '[[global:updated.title]]', + message: '[[global:updated.message]]', + clickfn: function() { + window.location.reload(); + }, + type: 'warning' + }); + } + }); + + // Admin keyboard shortcuts + require(['admin'], function(Admin) { + Admin.init(); + }); }); }; diff --git a/public/src/forum/account/edit.js b/public/src/forum/account/edit.js index 0a9237c7af..4b2645a976 100644 --- a/public/src/forum/account/edit.js +++ b/public/src/forum/account/edit.js @@ -1,6 +1,6 @@ 'use strict'; -/* globals define, ajaxify, socket, app, config, utils, translator */ +/* globals define, ajaxify, socket, app, config, utils, translator, bootbox */ define('forum/account/edit', ['forum/account/header', 'uploader'], function(header, uploader) { var AccountEdit = {}, @@ -26,6 +26,7 @@ define('forum/account/edit', ['forum/account/header', 'uploader'], function(head currentEmail = $('#inputEmail').val(); handleImageChange(); + handleAccountDelete(); handleImageUpload(); handleEmailConfirm(); handlePasswordChange(); @@ -125,6 +126,30 @@ define('forum/account/edit', ['forum/account/header', 'uploader'], function(head }); } + function handleAccountDelete() { + $('#deleteAccountBtn').on('click', function() { + translator.translate('[[user:delete_account_confirm]]', function(translated) { + bootbox.confirm(translated + '

', function(confirm) { + if (!confirm) { + return; + } + + if ($('#confirm-username').val() !== app.username) { + app.alertError('[[error:invalid-username]]'); + return false; + } else { + socket.emit('user.deleteAccount', {}, function(err) { + if (!err) { + app.logout(); + } + }); + } + }); + }); + return false; + }); + } + function handleImageUpload() { $('#upload-picture-modal').on('hide', function() { $('#userPhotoInput').val(''); diff --git a/public/src/forum/account/profile.js b/public/src/forum/account/profile.js index 343be867c2..2d273a33ca 100644 --- a/public/src/forum/account/profile.js +++ b/public/src/forum/account/profile.js @@ -2,7 +2,7 @@ /* globals define, ajaxify, app, utils, socket, translator*/ -define('forum/account/profile', ['forum/account/header'], function(header) { +define('forum/account/profile', ['forum/account/header', 'forum/infinitescroll'], function(header, infinitescroll) { var Account = {}, yourid, theirid, @@ -33,13 +33,14 @@ define('forum/account/profile', ['forum/account/header'], function(header) { app.openChat($('.account-username').html(), theirid); }); - socket.on('user.isOnline', handleUserOnline); - - socket.emit('user.isOnline', theirid, handleUserOnline); + socket.removeListener('event:user_status_change', onUserStatusChange); + socket.on('event:user_status_change', onUserStatusChange); if (yourid !== theirid) { socket.emit('user.increaseViewCount', theirid); } + + infinitescroll.init(loadMoreTopics); }; function processPage() { @@ -68,11 +69,7 @@ define('forum/account/profile', ['forum/account/header'], function(header) { return false; } - function handleUserOnline(err, data) { - if (err) { - return app.alertError(err.message); - } - + function onUserStatusChange(data) { var onlineStatus = $('.account-online-status'); if(parseInt(ajaxify.variables.get('theirid'), 10) !== parseInt(data.uid, 10)) { @@ -87,5 +84,44 @@ define('forum/account/profile', ['forum/account/header'], function(header) { } + function loadMoreTopics(direction) { + if(direction < 0 || !$('.user-recent-posts').length) { + return; + } + + $('.loading-indicator').removeClass('hidden'); + + infinitescroll.loadMore('user.loadMoreRecentPosts', { + after: $('.user-recent-posts').attr('data-nextstart'), + uid: theirid + }, function(data, done) { + if (data.posts && data.posts.length) { + onPostsLoaded(data.posts, done); + $('.user-recent-posts').attr('data-nextstart', data.nextStart); + } else { + done(); + } + $('.loading-indicator').addClass('hidden'); + }); + } + + function onPostsLoaded(posts, callback) { + posts = posts.filter(function(post) { + return !$('.user-recent-posts div[data-pid=' + post.pid + ']').length; + }); + + if (!posts.length) { + return callback(); + } + + infinitescroll.parseAndTranslate('account/profile', 'posts', {posts: posts}, function(html) { + + $('.user-recent-posts .loading-indicator').before(html); + html.find('span.timeago').timeago(); + + callback(); + }); + } + return Account; }); diff --git a/public/src/forum/admin/categories.js b/public/src/forum/admin/categories.js index cc1627fdf6..7c5dcf56b3 100644 --- a/public/src/forum/admin/categories.js +++ b/public/src/forum/admin/categories.js @@ -89,14 +89,8 @@ define('forum/admin/categories', ['uploader', 'forum/admin/iconSelect'], functio timeout: 2000 }); - ajaxify.loadTemplate('admin/categories', function(adminCategories) { - var html = $(templates.parse(templates.getBlock(adminCategories, 'categories'), {categories: [data]})); - - html.find('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker); - - $('#entry-container').append(html); - $('#new-category-modal').modal('hide'); - }); + $('#new-category-modal').modal('hide'); + ajaxify.refresh(); }); } @@ -203,7 +197,7 @@ define('forum/admin/categories', ['uploader', 'forum/admin/iconSelect'], functio var inputEl = $(this), cid = inputEl.parents('li[data-cid]').attr('data-cid'); - uploader.open(RELATIVE_PATH + '/admin/category/uploadpicture', {cid: cid}, 0, function(imageUrlOnServer) { + uploader.open(RELATIVE_PATH + '/admin/category/uploadpicture', { cid: cid }, 0, function(imageUrlOnServer) { inputEl.val(imageUrlOnServer); var previewBox = inputEl.parents('li[data-cid]').find('.preview-box'); previewBox.css('background', 'url(' + imageUrlOnServer + '?' + new Date().getTime() + ')') @@ -231,6 +225,45 @@ define('forum/admin/categories', ['uploader', 'forum/admin/iconSelect'], functio }); setupEditTargets(); + + $('button[data-action="setParent"]').on('click', function() { + var cid = $(this).parents('[data-cid]').attr('data-cid'), + modal = $('#setParent'); + + modal.find('select').val($(this).attr('data-parentCid')); + modal.attr('data-cid', cid).modal(); + }); + + $('button[data-action="removeParent"]').on('click', function() { + var cid = $(this).parents('[data-cid]').attr('data-cid'); + var payload= {}; + payload[cid] = { + parentCid: 0 + }; + socket.emit('admin.categories.update', payload, function(err) { + if (err) { + return app.alertError(err.message); + } + ajaxify.go('admin/categories/active'); + }); + }); + + $('#setParent [data-cid]').on('click', function() { + var modalEl = $('#setParent'), + parentCid = $(this).attr('data-cid'), + payload = {}; + + payload[modalEl.attr('data-cid')] = { + parentCid: parentCid + }; + + socket.emit('admin.categories.update', payload, function(err) { + modalEl.one('hidden.bs.modal', function() { + ajaxify.go('admin/categories/active'); + }); + modalEl.modal('hide'); + }); + }); }); }; diff --git a/public/src/forum/admin/groups.js b/public/src/forum/admin/groups.js index 34a1b59ed0..ee39ccf975 100644 --- a/public/src/forum/admin/groups.js +++ b/public/src/forum/admin/groups.js @@ -7,28 +7,53 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { Groups.init = function() { var yourid = ajaxify.variables.get('yourid'), createModal = $('#create-modal'), - createNameEl = $('#create-group-name'), - detailsModal = $('#group-details-modal'), - detailsSearch = detailsModal.find('#group-details-search'), - searchResults = detailsModal.find('#group-details-search-results'), - groupMembersEl = detailsModal.find('ul.current_members'), - detailsModalSave = detailsModal.find('.btn-primary'), - searchDelay, - listEl = $('#groups-list'); - - $('#create').on('click', function() { + createGroupName = $('#create-group-name'), + create = $('#create'), + createModalGo = $('#create-modal-go'), + createGroupDesc = $('#create-group-desc'), + createModalError = $('#create-modal-error'), + groupDetailsModal = $('#group-details-modal'), + groupDetailsSearch = $('#group-details-search'), + groupDetailsSearchResults = $('#group-details-search-results'), + groupMembersEl = $('ul.current_members'), + formEl = groupDetailsModal.find('form'), + detailsModalSave = $('#details-modal-save'), + groupsList = $('#groups-list'), + groupIcon = $('#group-icon'), + changeGroupIcon = $('#change-group-icon'), + changeGroupName = $('#change-group-name'), + changeGroupDesc = $('#change-group-desc'), + changeGroupUserTitle = $('#change-group-user-title'), + changeGroupLabelColor = $('#change-group-label-color'), + groupIcon = $('#group-icon'), + groupLabelPreview = $('#group-label-preview'), + searchDelay; + + // Tooltips + $('#groups-list .members li').tooltip(); + + createModal.on('keypress', function(e) { + switch(e.keyCode) { + case 13: + createModalGo.click(); + break; + default: + break; + } + }); + + create.on('click', function() { createModal.modal('show'); setTimeout(function() { - createNameEl.focus(); + createGroupName.focus(); }, 250); }); - $('#create-modal-go').on('click', function() { + createModalGo.on('click', function() { var submitObj = { - name: createNameEl.val(), - description: $('#create-group-desc').val() + name: createGroupName.val(), + description: createGroupDesc.val() }, - errorEl = $('#create-modal-error'), errorText; socket.emit('admin.groups.create', submitObj, function(err, data) { @@ -45,10 +70,10 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { break; } - errorEl.html(errorText).removeClass('hide'); + createModalError.html(errorText).removeClass('hide'); } else { - errorEl.addClass('hide'); - createNameEl.val(''); + createModalError.addClass('hide'); + createGroupName.val(''); createModal.on('hidden.bs.modal', function() { ajaxify.go('admin/groups'); }); @@ -57,7 +82,29 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { }); }); - listEl.on('click', 'button[data-action]', function() { + formEl.keypress(function(e) { + switch(e.keyCode) { + case 13: + detailsModalSave.click(); + break; + default: + break; + } + }); + + changeGroupUserTitle.keydown(function() { + setTimeout(function() { + groupLabelPreview.text(changeGroupUserTitle.val()); + }, 0); + }); + + changeGroupLabelColor.keydown(function() { + setTimeout(function() { + groupLabelPreview.css('background', changeGroupLabelColor.val() || '#000000'); + }, 0); + }); + + groupsList.on('click', 'button[data-action]', function() { var el = $(this), action = el.attr('data-action'), groupName = el.parents('li[data-groupname]').attr('data-groupname'); @@ -78,32 +125,12 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { break; case 'members': socket.emit('admin.groups.get', groupName, function(err, groupObj) { - var formEl = detailsModal.find('form').keypress(function(e) { - switch(e.keyCode) { - case 13: - detailsModalSave.click(); - break; - default: - break; - } - }), - groupLabelPreview = formEl.find('#group-label-preview'), - changeGroupUserTitle = formEl.find('#change-group-user-title'), - changeGroupLabelColor = formEl.find('#change-group-label-color'); - - formEl.find('#change-group-name').val(groupObj.name).prop('readonly', groupObj.system); - formEl.find('#change-group-desc').val(groupObj.description); - changeGroupUserTitle.val(groupObj.userTitle).keydown(function() { - setTimeout(function() { - groupLabelPreview.text(changeGroupUserTitle.val()); - }, 0); - }); - formEl.find('#group-icon').attr('class', 'fa fa-2x ' + groupObj.icon).attr('value', groupObj.icon); - changeGroupLabelColor.val(groupObj.labelColor).keydown(function() { - setTimeout(function() { - groupLabelPreview.css('background', changeGroupLabelColor.val() || '#000000'); - }, 0); - }); + + changeGroupName.val(groupObj.name).prop('readonly', groupObj.system); + changeGroupDesc.val(groupObj.description); + changeGroupUserTitle.val(groupObj.userTitle); + groupIcon.attr('class', 'fa fa-2x ' + groupObj.icon).attr('value', groupObj.icon); + changeGroupLabelColor.val(groupObj.labelColor); groupLabelPreview.css('background', groupObj.labelColor || '#000000').text(groupObj.userTitle); groupMembersEl.empty(); @@ -117,23 +144,21 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { } } - detailsModal.attr('data-groupname', groupObj.name); - detailsModal.modal('show'); + groupDetailsModal.attr('data-groupname', groupObj.name); + groupDetailsModal.modal('show'); }); break; } }); - detailsSearch.on('keyup', function() { - var searchEl = this; + groupDetailsSearch.on('keyup', function() { if (searchDelay) { clearTimeout(searchDelay); } searchDelay = setTimeout(function() { - var searchText = searchEl.value, - resultsEl = $('#group-details-search-results'), + var searchText = groupDetailsSearch.val(), foundUser; socket.emit('admin.user.search', searchText, function(err, results) { @@ -143,7 +168,7 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { numResults = 4; } - resultsEl.empty(); + groupDetailsSearchResults.empty(); for (x = 0; x < numResults; x++) { foundUser = $('
  • '); foundUser @@ -151,19 +176,19 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { .append($('').attr('src', results.users[x].picture)) .append($('').html(results.users[x].username)); - resultsEl.append(foundUser); + groupDetailsSearchResults.append(foundUser); } } else { - resultsEl.html('
  • No Users Found
  • '); + groupDetailsSearchResults.html('
  • No Users Found
  • '); } }); }, 200); }); - searchResults.on('click', 'li[data-uid]', function() { + groupDetailsSearchResults.on('click', 'li[data-uid]', function() { var userLabel = $(this), uid = parseInt(userLabel.attr('data-uid'), 10), - groupName = detailsModal.attr('data-groupname'), + groupName = groupDetailsModal.attr('data-groupname'), members = []; groupMembersEl.find('li[data-uid]').each(function() { @@ -184,7 +209,7 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { groupMembersEl.on('click', 'li[data-uid]', function() { var uid = $(this).attr('data-uid'), - groupName = detailsModal.attr('data-groupname'); + groupName = groupDetailsModal.attr('data-groupname'); socket.emit('admin.groups.get', groupName, function(err, groupObj){ if (!err){ @@ -204,38 +229,34 @@ define('forum/admin/groups', ['forum/admin/iconSelect'], function(iconSelect) { }); }); - $('#change-group-icon').on('click', function() { - iconSelect.init($('#group-icon')); + changeGroupIcon.on('click', function() { + iconSelect.init(groupIcon); }); - admin.enableColorPicker($('#change-group-label-color'), function(hsb, hex) { - $('#group-label-preview').css('background-color', '#' + hex); + admin.enableColorPicker(changeGroupLabelColor, function(hsb, hex) { + groupLabelPreview.css('background-color', '#' + hex); }); detailsModalSave.on('click', function() { - var formEl = detailsModal.find('form'); - socket.emit('admin.groups.update', { - groupName: detailsModal.attr('data-groupname'), + groupName: groupDetailsModal.attr('data-groupname'), values: { - name: formEl.find('#change-group-name').val(), - userTitle: formEl.find('#change-group-user-title').val(), - description: formEl.find('#change-group-desc').val(), - icon: formEl.find('#group-icon').attr('value'), - labelColor: formEl.find('#change-group-label-color').val() + name: changeGroupName.val(), + userTitle: changeGroupUserTitle.val(), + description: changeGroupDesc.val(), + icon: groupIcon.attr('value'), + labelColor: changeGroupLabelColor.val() } }, function(err) { if (!err) { - detailsModal.on('hidden.bs.modal', function() { + groupDetailsModal.on('hidden.bs.modal', function() { ajaxify.go('admin/groups'); }); - detailsModal.modal('hide'); + groupDetailsModal.modal('hide'); } }); }); - // Tooltips - $('#groups-list .members li').tooltip(); }; return Groups; diff --git a/public/src/forum/admin/index.js b/public/src/forum/admin/index.js index 29335ae7a8..01e3f8ddd8 100644 --- a/public/src/forum/admin/index.js +++ b/public/src/forum/admin/index.js @@ -3,19 +3,21 @@ define('forum/admin/index', ['semver'], function(semver) { var Admin = {}; - + var updateIntervalId = 0; Admin.init = function() { app.enterRoom('admin'); socket.emit('meta.rooms.getAll', Admin.updateRoomUsage); - socket.removeListener('event:meta.rooms.update', Admin.updateRoomUsage); - socket.on('event:meta.rooms.update', Admin.updateRoomUsage); + if (updateIntervalId) { + clearInterval(updateIntervalId); + } + updateIntervalId = setInterval(function() { + socket.emit('meta.rooms.getAll', Admin.updateRoomUsage); + }, 3000); $('#logout-link').on('click', function() { - $.post(RELATIVE_PATH + '/logout', { - _csrf: $('#csrf_token').val() - }, function() { + $.post(RELATIVE_PATH + '/logout', function() { window.location.href = RELATIVE_PATH + '/'; }); }); @@ -44,62 +46,72 @@ define('forum/admin/index', ['semver'], function(semver) { }); $('.restart').on('click', function() { - app.alert({ - timeout: 5000, - title: 'Restarting...', - message: 'NodeBB is restarting.', - type: 'info' + bootbox.confirm('Are you sure you wish to restart NodeBB?', function(confirm) { + if (confirm) { + app.alert({ + alert_id: 'instance_restart', + type: 'info', + title: 'Restarting... ', + message: 'NodeBB is restarting.', + timeout: 5000 + }); + + $(window).one('action:reconnected', function() { + app.alert({ + alert_id: 'instance_restart', + type: 'success', + title: ' Success', + message: 'NodeBB has successfully restarted.', + timeout: 5000 + }); + }); + + socket.emit('admin.restart'); + } }); + }); - $(window).one('action:reconnected', function() { - app.alertSuccess('NodeBB has successfully restarted.'); + $('.reload').on('click', function() { + app.alert({ + alert_id: 'instance_reload', + type: 'info', + title: 'Reloading... ', + message: 'NodeBB is reloading.', + timeout: 5000 }); - socket.emit('admin.restart'); + socket.emit('admin.reload', function(err) { + if (!err) { + app.alert({ + alert_id: 'instance_reload', + type: 'success', + title: ' Success', + message: 'NodeBB has successfully reloaded.', + timeout: 5000 + }); + } else { + app.alert({ + alert_id: 'instance_reload', + type: 'danger', + title: '[[global:alert.error]]', + message: '[[error:reload-failed, ' + err.message + ']]' + }); + } + }); }); }; Admin.updateRoomUsage = function(err, data) { - - function getUserCountIn(room) { - var count = 0; - for(var user in data[room]) { - if (data[room].hasOwnProperty(user)) { - ++count; - } - } - return count; + if (err) { + return app.alertError(err.message); } - var active_users = $('#active_users').html(''), - total = 0; - - if(!active_users.length) { - return; - } - - - var sortedData = []; - - for (var room in data) { - if (room !== '') { - sortedData.push({room: room, count: data[room].length}); - total += data[room].length; - } - } - - sortedData.sort(function(a, b) { - return parseInt(b.count, 10) - parseInt(a.count, 10); - }); - - var usersHtml = ''; - for(var i=0; i " + - sortedData[i].count + " active user" + (sortedData[i].count > 1 ? "s" : "") + ""; - } + var html = 'Online Users [ ' + data.onlineRegisteredCount + ' ]
    ' + + 'Online Guests [ ' + data.onlineGuestCount + ' ]
    ' + + 'Online Total [ ' + (data.onlineRegisteredCount + data.onlineGuestCount) + ' ]
    ' + + 'Socket Connections [ ' + data.socketCount + ' ]'; - active_users.html(usersHtml); - $('#connections').html(total); + $('#active_users').html(html); }; return Admin; diff --git a/public/src/forum/admin/tags.js b/public/src/forum/admin/tags.js new file mode 100644 index 0000000000..965ae0db13 --- /dev/null +++ b/public/src/forum/admin/tags.js @@ -0,0 +1,73 @@ +"use strict"; +/*global define, socket, app, admin, utils*/ + +define('forum/admin/tags', ['forum/infinitescroll'], function(infinitescroll) { + var Tags = {}; + var timeoutId = 0; + + Tags.init = function() { + handleColorPickers(); + + $('.tag-list').on('click', '.save', function() { + save($(this)); + }); + + $('#tag-search').on('input propertychange', function() { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = 0; + } + timeoutId = setTimeout(function() { + socket.emit('topics.searchAndLoadTags', {query: $('#tag-search').val()}, function(err, tags) { + if (err) { + return app.alertError(err.message); + } + infinitescroll.parseAndTranslate('admin/tags', 'tags', {tags: tags}, function(html) { + $('.tag-list').html(html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + timeoutId = 0; + }); + }); + }, 100); + }); + }; + + function handleColorPickers() { + function enableColorPicker(idx, inputEl) { + var $inputEl = $(inputEl), + previewEl = $inputEl.parents('.tag-row').find('.tag-item'); + + admin.enableColorPicker($inputEl, function(hsb, hex) { + if ($inputEl.attr('data-name') === 'bgColor') { + previewEl.css('background-color', '#' + hex); + } else if ($inputEl.attr('data-name') === 'color') { + previewEl.css('color', '#' + hex); + } + }); + } + + + $('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker); + } + + function save(saveBtn) { + var tagRow = saveBtn.parents('.tag-row'); + + var data = { + tag: tagRow.attr('data-tag'), + bgColor : tagRow.find('[data-name="bgColor"]').val(), + color : tagRow.find('[data-name="color"]').val() + }; + + socket.emit('admin.tags.update', data, function(err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('Tag Updated!'); + }); + } + + + + return Tags; +}); \ No newline at end of file diff --git a/public/src/forum/admin/users.js b/public/src/forum/admin/users.js index 4bd33c8657..554f78660b 100644 --- a/public/src/forum/admin/users.js +++ b/public/src/forum/admin/users.js @@ -73,6 +73,22 @@ define('forum/admin/users', function() { unselectAll(); }); + $('.reset-lockout').on('click', function() { + var uids = getSelectedUids(); + if (!uids.length) { + return; + } + + socket.emit('admin.user.resetLockouts', uids, function(err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('Lockout(s) reset!'); + }); + + unselectAll(); + }); + $('.admin-user').on('click', function() { var uids = getSelectedUids(); if (!uids.length) { diff --git a/public/src/forum/category.js b/public/src/forum/category.js index 81c082ba85..30bcf629f1 100644 --- a/public/src/forum/category.js +++ b/public/src/forum/category.js @@ -35,21 +35,39 @@ define('forum/category', ['composer', 'forum/pagination', 'forum/infinitescroll' enableInfiniteLoadingOrPagination(); if (!config.usePagination) { - navigator.init('#topics-container > .category-item', ajaxify.variables.get('topic_count'), undefined, Category.toTop, Category.toBottom); + navigator.init('#topics-container > .category-item', ajaxify.variables.get('topic_count'), Category.toTop, Category.toBottom, Category.navigatorCallback); } $('#topics-container').on('click', '.topic-title', function() { var clickedTid = $(this).parents('li.category-item[data-tid]').attr('data-tid'); $('#topics-container li.category-item').each(function(index, el) { if($(el).offset().top - $(window).scrollTop() > 0) { - localStorage.setItem('category:bookmark', $(el).attr('data-tid')); - localStorage.setItem('category:bookmark:clicked', clickedTid); + localStorage.setItem('category:' + cid + ':bookmark', $(el).attr('data-tid')); + localStorage.setItem('category:' + cid + ':bookmark:clicked', clickedTid); return false; } }); }); + + handleIgnoreWatch(cid); }; + function handleIgnoreWatch(cid) { + $('.watch, .ignore').on('click', function() { + var $this = $(this); + var command = $this.hasClass('watch') ? 'watch' : 'ignore'; + + socket.emit('categories.' + command, cid, function(err) { + if (err) { + return app.alertError(err.message); + } + + $('.watch').toggleClass('hidden', command === 'watch'); + $('.ignore').toggleClass('hidden', command === 'ignore'); + }); + }); + } + Category.toTop = function() { navigator.scrollTop(0); }; @@ -60,47 +78,59 @@ define('forum/category', ['composer', 'forum/pagination', 'forum/infinitescroll' }); }; + Category.navigatorCallback = function(element, elementCount) { + return parseInt(element.attr('data-index'), 10) + 1; + }; + $(window).on('action:popstate', function(ev, data) { if(data.url.indexOf('category/') === 0) { - var bookmark = localStorage.getItem('category:bookmark'); - var clicked = localStorage.getItem('category:bookmark:clicked'); + var cid = data.url.match(/^category\/(\d+)/); + if (cid && cid[1]) { + cid = cid[1]; + } + if (!cid) { + return; + } - if (bookmark) { + var bookmark = localStorage.getItem('category:' + cid + ':bookmark'); + var clicked = localStorage.getItem('category:' + cid + ':bookmark:clicked'); - if(config.usePagination) { - socket.emit('topics.getTidPage', bookmark, function(err, page) { - if(err) { - return; - } - if(parseInt(page, 10) !== pagination.currentPage) { - pagination.loadPage(page); - } else { - Category.scrollToTopic(bookmark, clicked, 400); - } - }); - } else { + if (!bookmark) { + return; + } - socket.emit('topics.getTidIndex', bookmark, function(err, index) { - if(err) { - return; - } + if(config.usePagination) { + socket.emit('topics.getTidPage', bookmark, function(err, page) { + if (err) { + return; + } + if(parseInt(page, 10) !== pagination.currentPage) { + pagination.loadPage(page); + } else { + Category.scrollToTopic(bookmark, clicked, 400); + } + }); + } else { + socket.emit('topics.getTidIndex', bookmark, function(err, index) { + if (err) { + return; + } - if(index === 0) { - Category.highlightTopic(clicked); - return; - } + if (index === 0) { + Category.highlightTopic(clicked); + return; + } - if (index < 0) { - index = 0; - } + if (index < 0) { + index = 0; + } - $('#topics-container').empty(); + $('#topics-container').empty(); - loadTopicsAfter(index, function() { - Category.scrollToTopic(bookmark, clicked, 0); - }); + loadTopicsAfter(index, function() { + Category.scrollToTopic(bookmark, clicked, 0); }); - } + }); } } }); @@ -216,10 +246,8 @@ define('forum/category', ['composer', 'forum/pagination', 'forum/infinitescroll' return; } - var topics = data.topics; - - function removeAlreadyAddedTopics() { - topics = topics.filter(function(topic) { + function removeAlreadyAddedTopics(topics) { + return topics.filter(function(topic) { return $('#topics-container li[data-tid="' + topic.tid +'"]').length === 0; }); } @@ -233,7 +261,7 @@ define('forum/category', ['composer', 'forum/pagination', 'forum/infinitescroll' } var last = $('#topics-container .category-item[data-tid]').last(); var lastIndex = last.attr('data-index'); - var firstIndex = topics[topics.length - 1].index; + var firstIndex = data.topics[data.topics.length - 1].index; if (firstIndex > lastIndex) { after = last; } else { @@ -241,8 +269,8 @@ define('forum/category', ['composer', 'forum/pagination', 'forum/infinitescroll' } } - removeAlreadyAddedTopics(); - if(!topics.length) { + data.topics = removeAlreadyAddedTopics(data.topics); + if(!data.topics.length) { return; } @@ -285,8 +313,7 @@ define('forum/category', ['composer', 'forum/pagination', 'forum/infinitescroll' return; } - infinitescroll.calculateAfter(direction, '#topics-container .category-item[data-tid]', config.topicsPerPage, function(after, offset, el) { - + infinitescroll.calculateAfter(direction, '#topics-container .category-item[data-tid]', config.topicsPerPage, false, function(after, offset, el) { loadTopicsAfter(after, function() { if (direction < 0 && el) { Category.scrollToTopic(el.attr('data-tid'), null, 0, offset); diff --git a/public/src/forum/chats.js b/public/src/forum/chats.js index d84dbac203..84bf4fd4cc 100644 --- a/public/src/forum/chats.js +++ b/public/src/forum/chats.js @@ -2,7 +2,7 @@ /* globals define, app, ajaxify, utils, socket, templates */ -define('forum/chats', ['string', 'sounds'], function(S, sounds) { +define('forum/chats', ['string', 'sounds', 'forum/infinitescroll'], function(S, sounds, infinitescroll) { var Chats = { initialised: false }; @@ -49,11 +49,13 @@ define('forum/chats', ['string', 'sounds'], function(S, sounds) { }); inputEl.on('keyup', function() { - if ($(this).val()) { - Chats.notifyTyping(Chats.getRecipientUid(), true); - } else { - Chats.notifyTyping(Chats.getRecipientUid(), false); + var val = !!$(this).val(); + if ((val && $(this).attr('data-typing') === 'true') || (!val && $(this).attr('data-typing') === 'false')) { + return; } + + Chats.notifyTyping(Chats.getRecipientUid(), val); + $(this).attr('data-typing', val); }); sendEl.on('click', function(e) { @@ -68,8 +70,36 @@ define('forum/chats', ['string', 'sounds'], function(S, sounds) { app.openChat(username, uid); }, true); }); + + $('.recent-chats').on('scroll', function() { + var $this = $(this); + var bottom = ($this[0].scrollHeight - $this.height()) * 0.9; + if ($this.scrollTop() > bottom) { + loadMoreRecentChats(); + } + }); + + $('.expanded-chat [data-since]').on('click', function() { + var since = $(this).attr('data-since'); + $('.expanded-chat [data-since]').removeClass('selected'); + $(this).addClass('selected'); + loadChatSince(since); + return false; + }); }; + function loadChatSince(since) { + var uid = Chats.getRecipientUid(); + if (!uid) { + return; + } + socket.emit('modules.chats.get', {touid: uid, since: since}, function(err, messages) { + var chatContent = $('.expanded-chat .chat-content'); + chatContent.find('.chat-message').remove(); + Chats.parseMessage(messages, onMessagesParsed); + }); + } + Chats.addGlobalEventListeners = function() { $(window).on('resize', Chats.resizeMainWindow); $(window).on('mousemove keypress click', function() { @@ -83,21 +113,23 @@ define('forum/chats', ['string', 'sounds'], function(S, sounds) { }); }; + function onMessagesParsed(html) { + var newMessage = $(html); + newMessage.insertBefore($('.user-typing')); + newMessage.find('span.timeago').timeago(); + newMessage.find('img:not(".chat-user-image")').addClass('img-responsive'); + Chats.scrollToBottom($('.expanded-chat .chat-content')); + } + Chats.addSocketListeners = function() { socket.on('event:chats.receive', function(data) { var typingNotifEl = $('.user-typing'), containerEl = $('.expanded-chat ul'); if (Chats.isCurrentChat(data.withUid)) { - newMessage = data.message.self === 0; - - Chats.parseMessage(data.message, function(html) { - var newMessage = $(html); - newMessage.insertBefore(typingNotifEl); - newMessage.find('span.timeago').timeago(); - newMessage.find('img:not(".chat-user-image")').addClass('img-responsive'); - Chats.scrollToBottom(containerEl); - }); + newMessage = data.self === 0; + data.message.self = data.self; + Chats.parseMessage(data.message, onMessagesParsed); } else { $('.chats-list li[data-uid="' + data.withUid + '"]').addClass('unread'); app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + data.message.fromUser.username + ']]'); @@ -123,6 +155,19 @@ define('forum/chats', ['string', 'sounds'], function(S, sounds) { $('.chats-list li[data-uid="' + withUid + '"]').removeClass('typing'); }); + + socket.on('event:user_status_change', function(data) { + var userEl = $('.chats-list li[data-uid="' + data.uid +'"]'); + + if (userEl.length) { + var statusEl = userEl.find('.status'); + translator.translate('[[global:' + data.status + ']]', function(translated) { + statusEl.attr('class', 'fa fa-circle status ' + data.status) + .attr('title', translated) + .attr('data-original-title', translated); + }); + } + }); }; Chats.resizeMainWindow = function() { @@ -170,6 +215,7 @@ define('forum/chats', ['string', 'sounds'], function(S, sounds) { var recipientUid = Chats.getRecipientUid(); if (recipientUid) { socket.emit('modules.chats.markRead', recipientUid); + $('.expanded-chat input').focus(); } $('.chats-list li').removeClass('bg-primary'); $('.chats-list li[data-uid="' + recipientUid + '"]').addClass('bg-primary'); @@ -181,5 +227,44 @@ define('forum/chats', ['string', 'sounds'], function(S, sounds) { }, callback); }; + function loadMoreRecentChats() { + var recentChats = $('.recent-chats'); + if (recentChats.attr('loading')) { + return; + } + recentChats.attr('loading', 1); + socket.emit('modules.chats.getRecentChats', { + after: recentChats.attr('data-nextstart') + }, function(err, data) { + if (err) { + return app.alertError(err.message); + } + + if (data && data.users.length) { + onRecentChatsLoaded(data.users, function() { + recentChats.removeAttr('loading'); + recentChats.attr('data-nextstart', data.nextStart); + }); + } else { + recentChats.removeAttr('loading'); + } + }); + } + + function onRecentChatsLoaded(users, callback) { + users = users.filter(function(user) { + return !$('.recent-chats li[data-uid=' + user.uid + ']').length; + }); + + if (!users.length) { + return callback(); + } + + infinitescroll.parseAndTranslate('chats', 'chats', {chats: users}, function(html) { + $('.recent-chats').append(html); + callback(); + }); + } + return Chats; }); diff --git a/public/src/forum/footer.js b/public/src/forum/footer.js index e7e8209a1b..775686aaa2 100644 --- a/public/src/forum/footer.js +++ b/public/src/forum/footer.js @@ -24,10 +24,46 @@ define('forum/footer', ['notifications', 'chat'], function(Notifications, Chat) .attr('data-content', count > 20 ? '20+' : count); } + function initUnreadTopics() { + var unreadTopics = {}; + + function onNewPost(data) { + if (data && data.posts && data.posts.length) { + var post = data.posts[0]; + + if (parseInt(post.uid, 10) !== parseInt(app.uid, 10) && !unreadTopics[post.topic.tid]) { + increaseUnreadCount(); + markTopicsUnread(post.topic.tid); + unreadTopics[post.topic.tid] = true; + } + } + } + + function increaseUnreadCount() { + var count = parseInt($('#unread-count').attr('data-content'), 10) + 1; + updateUnreadTopicCount(null, count); + } + + function markTopicsUnread(tid) { + $('[data-tid="' + tid + '"]').addClass('unread'); + } + + $(window).on('action:ajaxify.end', function(ev, data) { + var tid = data.url.match(/^topic\/(\d+)/); + + if (tid && tid[1]) { + delete unreadTopics[tid[1]]; + } + }); + + socket.on('event:new_post', onNewPost); + } socket.on('event:unread.updateCount', updateUnreadTopicCount); socket.emit('user.getUnreadCount', updateUnreadTopicCount); socket.on('event:unread.updateChatCount', updateUnreadChatCount); socket.emit('user.getUnreadChatCount', updateUnreadChatCount); + + initUnreadTopics(); }); diff --git a/public/src/forum/home.js b/public/src/forum/home.js index 461b5a9c8f..c770aceb36 100644 --- a/public/src/forum/home.js +++ b/public/src/forum/home.js @@ -24,16 +24,8 @@ define('forum/home', function() { }; home.onNewPost = function(data) { - - if (data && data.posts && data.posts.length) { - - socket.emit('posts.getCategory', data.posts[0].pid, function(err, cid) { - if (err) { - return; - } - - renderNewPost(cid, data.posts[0]); - }); + if (data && data.posts && data.posts.length && data.posts[0].topic) { + renderNewPost(data.posts[0].topic.cid, data.posts[0]); } }; diff --git a/public/src/forum/infinitescroll.js b/public/src/forum/infinitescroll.js index 053635f312..ac0787a023 100644 --- a/public/src/forum/infinitescroll.js +++ b/public/src/forum/infinitescroll.js @@ -17,9 +17,10 @@ define('forum/infinitescroll', function() { }; function onScroll() { - var top = $(window).height() * 0.1 + topOffset; - var bottom = ($(document).height() - $(window).height()) * 0.9; - var currentScrollTop = $(window).scrollTop(); + var originalPostEl = $('li[data-index="0"]'), + top = $(window).height() * 0.15 + topOffset + (originalPostEl ? originalPostEl.outerHeight() : 0), + bottom = ($(document).height() - $(window).height()) * 0.85, + currentScrollTop = $(window).scrollTop(); if(currentScrollTop < top && currentScrollTop < previousScrollTop) { callback(-1); @@ -55,13 +56,16 @@ define('forum/infinitescroll', function() { }); }; - scroll.calculateAfter = function(direction, selector, count, callback) { + scroll.calculateAfter = function(direction, selector, count, reverse, callback) { var after = 0, offset = 0, el = direction > 0 ? $(selector).last() : $(selector).first(); + var count = reverse ? -count : count; + var increment = reverse ? -1 : 1; + if (direction > 0) { - after = parseInt(el.attr('data-index'), 10) + 1; + after = parseInt(el.attr('data-index'), 10) + increment; } else { after = parseInt(el.attr('data-index'), 10); if (isNaN(after)) { diff --git a/public/src/forum/login.js b/public/src/forum/login.js index 4f4c9b6061..c4600c36ac 100644 --- a/public/src/forum/login.js +++ b/public/src/forum/login.js @@ -5,64 +5,31 @@ define('forum/login', function() { var Login = {}; Login.init = function() { - $('#login').on('click', function(e) { - e.preventDefault(); - - var loginData = { - 'username': $('#username').val(), - 'password': $('#password').val(), - 'remember': $('#remember').prop('checked'), - '_csrf': $('#csrf-token').val() - }, - previousUrl = $('input[name="previousUrl"]').val(); - - $('#login').attr('disabled', 'disabled').html('Logging in...'); - $('#login-error-notify').hide(); + var errorEl = $('#login-error-notify'), + submitEl = $('#login'), + formEl = $('#login-form'); - $.ajax({ - type: "POST", - url: RELATIVE_PATH + '/login', - data: loginData, - success: function(data, textStatus, jqXHR) { - $('#login').html('Redirecting...'); - if (previousUrl) { - app.previousUrl = previousUrl; - } else if (!app.previousUrl) { - app.previousUrl = RELATIVE_PATH || '/'; - } - - if(app.previousUrl.indexOf('/reset/') !== -1) { - window.location.replace(RELATIVE_PATH + "/?loggedin"); - } else { - var index = app.previousUrl.indexOf('#'); - if(index !== -1) { - window.location.replace(app.previousUrl.slice(0, index) + '?loggedin' + app.previousUrl.slice(index)); - } else { - window.location.replace(app.previousUrl + "?loggedin"); - } - } - - app.loadConfig(); - }, - error: function(data, textStatus, jqXHR) { - var message = data.responseJSON; - if (typeof data.responseJSON !== 'string') { - message = data.responseJSON.message || ''; - } - translator.translate(message, function(errorText) { - $('#login-error-notify').show().html(errorText); - }); + submitEl.on('click', function(e) { + e.preventDefault(); - $('#login').removeAttr('disabled').html('Login'); - }, - dataType: 'json', - async: true - }); + if (!$('#username').val() || !$('#password').val()) { + translator.translate('[[error:invalid-username-or-password]]', function(translated) { + errorEl.find('p').text(translated) + errorEl.show(); + }); + } else { + errorEl.hide(); + + if (!submitEl.hasClass('disabled')) { + submitEl.addClass('disabled'); + formEl.submit(); + } + } }); $('#login-error-notify button').on('click', function(e) { e.preventDefault(); - $('#login-error-notify').hide(); + errorEl.hide(); }); $('#content #username').focus(); diff --git a/public/src/forum/popular.js b/public/src/forum/popular.js index 37729dc90b..7495f5110d 100644 --- a/public/src/forum/popular.js +++ b/public/src/forum/popular.js @@ -5,12 +5,6 @@ define('forum/popular', ['forum/recent', 'forum/infinitescroll'], function(recent, infinitescroll) { var Popular = {}; - $(window).on('action:ajaxify.start', function(ev, data) { - if(data.url.indexOf('recent') !== 0) { - recent.removeListeners(); - } - }); - Popular.init = function() { app.enterRoom('recent_posts'); @@ -18,8 +12,6 @@ define('forum/popular', ['forum/recent', 'forum/infinitescroll'], function(recen $(this).addClass('hide'); }); - recent.watchForNewPosts(); - recent.selectActivePill(); }; diff --git a/public/src/forum/recent.js b/public/src/forum/recent.js index b4a27e664b..78cbff6c14 100644 --- a/public/src/forum/recent.js +++ b/public/src/forum/recent.js @@ -116,6 +116,15 @@ define('forum/recent', ['forum/infinitescroll'], function(infinitescroll) { }; Recent.onTopicsLoaded = function(templateName, topics, showSelect, callback) { + + topics = topics.filter(function(topic) { + return !$('#topics-container li[data-tid=' + topic.tid + ']').length; + }); + + if (!topics.length) { + return callback(); + } + infinitescroll.parseAndTranslate(templateName, 'topics', {topics: topics, showSelect: showSelect}, function(html) { $('#category-no-topics').remove(); diff --git a/public/src/forum/search.js b/public/src/forum/search.js index 38a57eb414..861d8dd441 100644 --- a/public/src/forum/search.js +++ b/public/src/forum/search.js @@ -1,4 +1,4 @@ -define('forum/search', function() { +define('forum/search', ['search'], function(searchModule) { var Search = {}; Search.init = function() { @@ -21,11 +21,13 @@ define('forum/search', function() { $('#search-form input').val(searchQuery); - $('#mobile-search-form').off('submit').on('submit', function() { + $('#mobile-search-form').off('submit').on('submit', function(e) { + e.preventDefault(); var input = $(this).find('input'); - ajaxify.go('search/' + input.val(), null, 'search'); - input.val(''); - return false; + + searchModule.query(input.val(), function() { + input.val(''); + }); }); }; diff --git a/public/src/forum/tags.js b/public/src/forum/tags.js index c96a921472..bf4395f497 100644 --- a/public/src/forum/tags.js +++ b/public/src/forum/tags.js @@ -1,20 +1,59 @@ 'use strict'; -/* globals define, app */ +/* globals define, app, utils, socket */ -define('forum/tags', function() { +define('forum/tags', ['forum/infinitescroll'], function(infinitescroll) { var Tags = {}; + var timeoutId = 0; Tags.init = function() { app.enterRoom('tags'); $('#tag-search').on('input propertychange', function() { - $('.tag-list').children().each(function() { - var $this = $(this); - $this.toggleClass('hide', $this.find('a').attr('data-value').indexOf($('#tag-search').val()) === -1); - }) + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = 0; + } + timeoutId = setTimeout(function() { + socket.emit('topics.searchAndLoadTags', {query: $('#tag-search').val()}, function(err, results) { + if (err) { + return app.alertError(err.message); + } + onTagsLoaded(results, true, function() { + timeoutId = 0; + }); + }); + }, 100); }); + + infinitescroll.init(Tags.loadMoreTags); }; + Tags.loadMoreTags = function(direction) { + if(direction < 0 || !$('.tag-list').length) { + return; + } + + infinitescroll.loadMore('topics.loadMoreTags', { + after: $('.tag-list').attr('data-nextstart') + }, function(data, done) { + if (data && data.tags && data.tags.length) { + onTagsLoaded(data.tags, false, done); + $('.tag-list').attr('data-nextstart', data.nextStart); + } else { + done(); + } + }); + }; + + function onTagsLoaded(tags, replace, callback) { + callback = callback || function() {}; + infinitescroll.parseAndTranslate('tags', 'tags', {tags: tags}, function(html) { + $('.tag-list')[replace ? 'html' : 'append'](html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + callback(); + }); + } + return Tags; }); diff --git a/public/src/forum/topic.js b/public/src/forum/topic.js index 0300d58121..bcfdc0187c 100644 --- a/public/src/forum/topic.js +++ b/public/src/forum/topic.js @@ -63,7 +63,7 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT handleBookmark(tid); - navigator.init('.posts > .post-row', postCount, Topic.navigatorCallback, Topic.toTop, Topic.toBottom); + navigator.init('.posts > .post-row', postCount, Topic.toTop, Topic.toBottom, Topic.navigatorCallback, Topic.calculateIndex); socket.on('event:new_post', onNewPost); socket.on('event:new_notification', onNewNotification); @@ -80,8 +80,11 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT }; Topic.toBottom = function() { - socket.emit('topics.lastPostIndex', ajaxify.variables.get('topic_id'), function(err, index) { - navigator.scrollBottom(index); + socket.emit('topics.postcount', ajaxify.variables.get('topic_id'), function(err, postCount) { + if (config.topicPostSort !== 'oldest_to_newest') { + postCount = 1; + } + navigator.scrollBottom(postCount); }); }; @@ -146,7 +149,7 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT var postcount = $('.user_postcount_' + data.posts[i].uid); postcount.html(parseInt(postcount.html(), 10) + 1); } - socket.emit('topics.markAsRead', tid); + socket.emit('topics.markAsRead', [tid]); createNewPosts(data); } @@ -201,8 +204,27 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT } } - Topic.navigatorCallback = function(element) { + Topic.calculateIndex = function(index, elementCount) { + if (index !== 1 && config.topicPostSort !== 'oldest_to_newest') { + return elementCount - index + 2; + } + return index; + }; + + Topic.navigatorCallback = function(element, elementCount) { + var path = ajaxify.removeRelativePath(window.location.pathname.slice(1)); + if (!path.startsWith('topic')) { + return 1; + } var postIndex = parseInt(element.attr('data-index'), 10); + var index = postIndex + 1; + if (config.topicPostSort !== 'oldest_to_newest') { + if (postIndex === 0) { + index = 1; + } else { + index = Math.max(elementCount - postIndex + 1, 1); + } + } var currentBookmark = localStorage.getItem('topic:' + ajaxify.variables.get('topic_id') + ':bookmark'); @@ -230,6 +252,7 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT currentUrl = newUrl; } } + return index; }; function onNewPostPagination(data) { @@ -247,8 +270,9 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT } function createNewPosts(data, callback) { + callback = callback || function() {}; if(!data || (data.posts && !data.posts.length)) { - return; + return callback(false); } function removeAlreadyAddedPosts() { @@ -297,7 +321,7 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT removeAlreadyAddedPosts(); if(!data.posts.length) { - return; + return callback(false); } findInsertionPoint(); @@ -309,7 +333,19 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT if(after) { html.insertAfter(after); } else if(before) { + // Save document height and position for future reference (about 5 lines down) + var height = $(document).height(), + scrollTop = $(document).scrollTop(), + originalPostEl = $('li[data-index="0"]'); + + // Insert the new post html.insertBefore(before); + + // If the user is not at the top of the page... (or reasonably so...) + if (scrollTop > originalPostEl.offset().top) { + // Now restore the relative position the user was on prior to new post insertion + $(document).scrollTop(scrollTop + ($(document).height() - height)); + } } else { $('#post-container').append(html); } @@ -320,9 +356,7 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT $(window).trigger('action:posts.loaded'); onNewPostsLoaded(html, data.posts); - if (typeof callback === 'function') { - callback(); - } + callback(true); }); } @@ -344,7 +378,6 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT } function processPage(element) { - browsing.populateOnlineUsers(); app.createUserTooltips(); app.replaceSelfLinks(element.find('a')); utils.addCommasToNumbers(element.find('.formatted-number')); @@ -373,16 +406,14 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT return; } - infinitescroll.calculateAfter(direction, '#post-container .post-row[data-index!="0"]', config.postsPerPage, function(after, offset, el) { - loadPostsAfter(after, function() { - if (direction < 0 && el) { - navigator.scrollToPost(el.attr('data-index'), false, 0, offset); - } - }); + var reverse = config.topicPostSort === 'newest_to_oldest' || config.topicPostSort === 'most_votes'; + + infinitescroll.calculateAfter(direction, '#post-container .post-row[data-index!="0"]', config.postsPerPage, reverse, function(after, offset, el) { + loadPostsAfter(after); }); } - function loadPostsAfter(after, callback) { + function loadPostsAfter(after) { var tid = ajaxify.variables.get('topic_id'); if (!utils.isNumber(tid) || !utils.isNumber(after) || (after === 0 && $('#post-container li.post-row[data-index="1"]').length)) { return; @@ -401,9 +432,8 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT indicatorEl.fadeOut(); if (data && data.posts && data.posts.length) { - createNewPosts(data, function() { + createNewPosts(data, function(postsCreated) { done(); - callback(); }); hidePostToolsForDeletedPosts(); } else { diff --git a/public/src/forum/topic/browsing.js b/public/src/forum/topic/browsing.js index 3a2a9e3160..d2cebbcf14 100644 --- a/public/src/forum/topic/browsing.js +++ b/public/src/forum/topic/browsing.js @@ -10,128 +10,73 @@ define('forum/topic/browsing', function() { Browsing.onUpdateUsersInRoom = function(data) { if(data && data.room.indexOf('topic_' + ajaxify.variables.get('topic_id')) !== -1) { - var activeEl = $('.thread_active_users'); - - // remove users that are no longer here - activeEl.find('a').each(function(index, element) { - if(element) { - var uid = $(element).attr('data-uid'); - var absent = data.users.every(function(user) { - return parseInt(user.uid, 10) !== parseInt(uid, 10); - }); - - if (absent) { - $(element).parent().remove(); - } - } - }); - - var i=0, icon; - // add self - for(i = 0; i 8) { - break; - } - } - - activeEl.find('a[data-uid] img').tooltip({ - placement: 'top' - }); - - var remainingUsers = data.users.length - 9; - remainingUsers = remainingUsers < 0 ? 0 : remainingUsers; - var anonymousCount = parseInt(data.anonymousCount, 10); - activeEl.find('.anonymous-box').remove(); - if(anonymousCount || remainingUsers) { - - var anonLink = $('
    '); - activeEl.append(anonLink); - - var title = ''; - if(remainingUsers && anonymousCount) { - title = '[[topic:more_users_and_guests, ' + remainingUsers + ', ' + anonymousCount + ']]'; - } else if(remainingUsers) { - title = '[[topic:more_users, ' + remainingUsers + ']]'; - } else { - title = '[[topic:more_guests, ' + anonymousCount + ']]'; - } - - translator.translate(title, function(translated) { - $('.anonymous-box').tooltip({ - placement: 'top', - title: translated - }); - }); + for(var i=0; i'); + return $('
    '); } } diff --git a/public/src/forum/topic/events.js b/public/src/forum/topic/events.js index e1d7becc8c..ec61a8ff0e 100644 --- a/public/src/forum/topic/events.js +++ b/public/src/forum/topic/events.js @@ -1,7 +1,7 @@ 'use strict'; -/* globals app, ajaxify, define, socket, translator */ +/* globals app, ajaxify, define, socket, translator, templates */ define('forum/topic/events', ['forum/topic/browsing', 'forum/topic/postTools', 'forum/topic/threadTools'], function(browsing, postTools, threadTools) { @@ -9,7 +9,9 @@ define('forum/topic/events', ['forum/topic/browsing', 'forum/topic/postTools', ' var events = { 'event:update_users_in_room': browsing.onUpdateUsersInRoom, - 'user.isOnline': browsing.onUserOnline, + 'event:user_enter': browsing.onUserEnter, + 'event:user_leave': browsing.onUserLeave, + 'event:user_status_change': browsing.onUserStatusChange, 'event:voted': updatePostVotesAndUserReputation, 'event:favourited': updateFavouriteCount, diff --git a/public/src/forum/topic/fork.js b/public/src/forum/topic/fork.js index 38052fa35b..6c462b4566 100644 --- a/public/src/forum/topic/fork.js +++ b/public/src/forum/topic/fork.js @@ -42,18 +42,26 @@ define('forum/topic/fork', function() { socket.emit('topics.createTopicFromPosts', { title: forkModal.find('#fork-title').val(), pids: pids - }, function(err) { + }, function(err, newTopic) { function fadeOutAndRemove(pid) { $('#post-container li[data-pid="' + pid + '"]').fadeOut(500, function() { $(this).remove(); }); } - if(err) { + if (err) { return app.alertError(err.message); } - app.alertSuccess('[[topic:fork_success]]'); + app.alert({ + timeout: 5000, + title: '[[global:alert.success]]', + message: '[[topic:fork_success]]', + type: 'success', + clickfn: function() { + ajaxify.go('topic/' + newTopic.slug); + } + }); for(var i=0; i') - .css({background: info.bgColor, color: info.color || '#fff'}) - .toggleClass('disabled', info.disabled) - .attr('data-cid', info.cid) - .html(' ' + info.name) - .appendTo(categoriesEl); - } - - $('#categories-loading').remove(); + templates.parse('partials/category_list', {categories: categories}, function(html) { + modal.find('.modal-body').prepend(html); + $('#categories-loading').remove(); + }); } return Move; diff --git a/public/src/forum/topic/postTools.js b/public/src/forum/topic/postTools.js index 5fe564c3db..96edf0720c 100644 --- a/public/src/forum/topic/postTools.js +++ b/public/src/forum/topic/postTools.js @@ -14,7 +14,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com share.addShareHandlers(topicName); - addFavouriteHandler(); + addVoteHandler(); }; PostTools.toggle = function(pid, isDeleted) { @@ -35,32 +35,38 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com }); }; - function addFavouriteHandler() { - $('#post-container').on('mouseenter', '.favourite-tooltip', function() { - if (!$(this).data('users-loaded')) { - $(this).data('users-loaded', 'true'); - var pid = $(this).parents('.post-row').attr('data-pid'); - var el = $(this).attr('title', "Loading..."); - socket.emit('posts.getFavouritedUsers', pid, function(err, usernames) { - if (err) { - return; - } - if (usernames.length > 6) { - var otherCount = usernames.length - 5; - usernames = usernames.slice(0, 5).join(', ').replace(/,/g, '|'); - translator.translate('[[topic:users_and_others, ' + usernames + ', ' + otherCount + ']]', function(translated) { - translated = translated.replace(/\|/g, ','); - el.attr('title', translated).tooltip('show'); - }); - } else { - usernames = usernames.join(', '); - el.attr('title', usernames).tooltip('show'); - } - }); + function addVoteHandler() { + $('#post-container').on('mouseenter', '.post-row .votes', function() { + loadDataAndCreateTooltip($(this), 'posts.getUpvoters'); + }); + } + + function loadDataAndCreateTooltip(el, method) { + var pid = el.parents('.post-row').attr('data-pid'); + socket.emit(method, pid, function(err, data) { + if (!err) { + createTooltip(el, data); } }); } + function createTooltip(el, data) { + var usernames = data.usernames; + if (!usernames.length) { + return; + } + if (usernames.length + data.otherCount > 6) { + usernames = usernames.join(', ').replace(/,/g, '|'); + translator.translate('[[topic:users_and_others, ' + usernames + ', ' + data.otherCount + ']]', function(translated) { + translated = translated.replace(/\|/g, ','); + el.attr('title', translated).tooltip('destroy').tooltip('show'); + }); + } else { + usernames = usernames.join(', '); + el.attr('title', usernames).tooltip('destroy').tooltip('show'); + } + } + function addPostHandlers(tid, threadState) { $('.topic').on('click', '.post_reply', function() { if (!threadState.locked) { @@ -125,7 +131,9 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com } var username = getUserName(selectionText ? $(selection.baseNode) : button); - + if (getData(button, 'data-uid') === '0') { + username = ''; + } if (selectionText.length) { composer.addQuote(tid, ajaxify.variables.get('topic_slug'), getData(button, 'data-index'), getData(button, 'data-pid'), topicName, username, selectionText); } else { @@ -195,8 +203,12 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com post = button.parents('li[data-pid]'); if (post.length) { - username = '@' + post.attr('data-username').replace(/\s/g, '-'); + username = post.attr('data-username').replace(/\s/g, '-'); } + if (post.length && post.attr('data-uid') !== '0') { + username = '@' + username; + } + return username; } diff --git a/public/src/forum/unread.js b/public/src/forum/unread.js index f8e1a9be63..342102f634 100644 --- a/public/src/forum/unread.js +++ b/public/src/forum/unread.js @@ -25,7 +25,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll'], if(!tids.length) { return; } - socket.emit('topics.markTidsRead', tids, function(err) { + socket.emit('topics.markAsRead', tids, function(err) { if(err) { return app.alertError(err.message); } @@ -45,11 +45,6 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll'], $('#topics-container').empty(); $('#category-no-topics').removeClass('hidden'); $('.markread').addClass('hidden'); - - $('#numUnreadBadge') - .removeClass('badge-important') - .addClass('badge-inverse') - .html('0'); }); }); diff --git a/public/src/forum/users.js b/public/src/forum/users.js index e30ab1ae33..77a56c7b51 100644 --- a/public/src/forum/users.js +++ b/public/src/forum/users.js @@ -22,14 +22,8 @@ define('forum/users', function() { handleSearch(); - socket.removeListener('user.anonDisconnect', updateAnonCount); - socket.removeListener('user.anonConnect', updateAnonCount); - socket.removeListener('user.isOnline', onUserIsOnline); - - socket.on('user.anonDisconnect', updateAnonCount); - socket.on('user.anonConnect', updateAnonCount); - socket.on('user.isOnline', onUserIsOnline); - + socket.removeListener('event:user_status_change', onUserStatusChange); + socket.on('event:user_status_change', onUserStatusChange); $('#load-more-users-btn').on('click', loadMoreUsers); @@ -78,6 +72,10 @@ define('forum/users', function() { } function onUsersLoaded(users) { + users = users.filter(function(user) { + return !$('.users-box[data-uid="' + user.uid + '"]').length; + }); + ajaxify.loadTemplate('users', function(usersTemplate) { var html = templates.parse(templates.getBlock(usersTemplate, 'users'), {users: users}); @@ -154,51 +152,26 @@ define('forum/users', function() { }); } - function onUserIsOnline(err, data) { + function onUserStatusChange(data) { var section = getActiveSection(); - if((section.indexOf('online') === 0 || section.indexOf('users') === 0) && !loadingMoreUsers) { + if((section.indexOf('online') === 0 || section.indexOf('users') === 0)) { updateUser(data); - updateAnonCount(); } } function updateUser(data) { - var usersContainer = $('#users-container'); - var userEl = usersContainer.find('li[data-uid="' + data.uid +'"]'); - if (!data.online) { - userEl.remove(); + if (data.status === 'offline') { return; } + var usersContainer = $('#users-container'); + var userEl = usersContainer.find('li[data-uid="' + data.uid +'"]'); - ajaxify.loadTemplate('users', function(usersTemplate) { - var html = templates.parse(templates.getBlock(usersTemplate, 'users'), {users: [data]}); - translator.translate(html, function(translated) { - if (userEl.length) { - userEl.replaceWith(translated); - return; - } - - var anonBox = usersContainer.find('li.anon-user'); - if (anonBox.length) { - $(translated).insertBefore(anonBox); - } else { - usersContainer.append(translated); - } - }); - }); - } - - function updateAnonCount() { - var section = getActiveSection(); - if((section.indexOf('online') === 0 || section.indexOf('users') === 0) && !loadingMoreUsers) { - socket.emit('user.getOnlineAnonCount', {} , function(err, anonCount) { - - if(parseInt(anonCount, 10) > 0) { - $('#users-container .anon-user').removeClass('hide'); - $('#online_anon_count').html(anonCount); - } else { - $('#users-container .anon-user').addClass('hide'); - } + if (userEl.length) { + var statusEl = userEl.find('.status'); + translator.translate('[[global:' + data.status + ']]', function(translated) { + statusEl.attr('class', 'fa fa-circle status ' + data.status) + .attr('title', translated) + .attr('data-original-title', translated); }); } } diff --git a/public/src/modules/admin.js b/public/src/modules/admin.js new file mode 100644 index 0000000000..8b447942be --- /dev/null +++ b/public/src/modules/admin.js @@ -0,0 +1,19 @@ +define('admin', ['mousetrap'], function(Mousetrap) { + var Admin= {}; + + Admin.init = function() { + if (app.isAdmin) { + Mousetrap.bind('ctrl+shift+a r', function() { + console.log('[admin] Reloading NodeBB...'); + socket.emit('admin.reload'); + }); + + Mousetrap.bind('ctrl+shift+a R', function() { + console.log('[admin] Restarting NodeBB...'); + socket.emit('admin.restart'); + }); + } + }; + + return Admin; +}); \ No newline at end of file diff --git a/public/src/modules/alerts.js b/public/src/modules/alerts.js index 9cf301f5af..ccbec68d0a 100644 --- a/public/src/modules/alerts.js +++ b/public/src/modules/alerts.js @@ -2,12 +2,13 @@ /* globals define, translator, templates */ define('alerts', function() { - var module = {}; module.alert = function (params) { params.alert_id = 'alert_button_' + (params.alert_id ? params.alert_id : new Date().getTime()); - params.title = params.title || ''; + params.title = params.title ? params.title.trim() || '' : ''; + params.message = params.message.trim(); + params.location = params.location || 'right-top'; params.type = params.type || 'info'; @@ -22,7 +23,11 @@ define('alerts', function() { function createNew(params) { templates.parse('alert', params, function(alertTpl) { translator.translate(alertTpl, function(translatedHTML) { - var alert = $(translatedHTML); + var alert = $('#' + params.alert_id); + if (alert.length) { + return updateAlert(alert, params); + } + alert = $(translatedHTML); alert.fadeIn(200); $('.alert-' + params.location).prepend(alert); @@ -63,7 +68,9 @@ define('alerts', function() { alert.attr('class', 'alert alert-dismissable alert-' + params.type); clearTimeout(parseInt(alert.attr('timeoutId'), 10)); - startTimeout(alert, params.timeout); + if (params.timeout) { + startTimeout(alert, params.timeout); + } alert.children().fadeOut(100); translator.translate(alert.html(), function(translatedHTML) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index cc4fbb1f42..452610c245 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -7,7 +7,6 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, var newMessage = false; module.prepareDOM = function() { - // Chats Dropdown var chatsToggleEl = $('#chat_dropdown'), chatsListEl = $('#chat-list'); @@ -16,11 +15,11 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, return; } - socket.emit('modules.chats.list', function(err, chats) { + socket.emit('modules.chats.getRecentChats', {after: 0}, function(err, chats) { if (err) { return app.alertError(err.message); } - + chats = chats.users; var userObj; chatsListEl.empty(); @@ -64,17 +63,17 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, var username = data.message.fromUser.username; var isSelf = parseInt(data.message.fromUser.uid, 10) === parseInt(app.uid, 10); + data.message.self = data.self; if (isSelf) { username = data.message.toUser.username; } - newMessage = data.message.self === 0; + newMessage = data.self === 0; if (module.modalExists(data.withUid)) { var modal = module.getModal(data.withUid); module.appendChatMessage(modal, data.message); if (modal.is(":visible")) { module.bringModalToTop(modal); - checkOnlineStatus(modal); taskbar.updateActive(modal.attr('UUID')); Chats.scrollToBottom(modal.find('#chat-content')); } else { @@ -114,6 +113,10 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, var modal = module.getModal(withUid); modal.find('.user-typing').addClass('hide'); }); + + socket.on('event:user_status_change', function(data) { + updateStatus(data.status); + }); }; module.bringModalToTop = function(chatModal) { @@ -125,6 +128,7 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, } }); chatModal.css('zIndex', topZ + 1); + taskbar.updateActive(chatModal.attr('UUID')); }; module.getModal = function(touid) { @@ -136,21 +140,20 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, }; function checkStatus(chatModal) { - socket.emit('user.isOnline', chatModal.attr('touid'), function(err, data) { - translator.translate('[[global:' + data.status + ']]', function(translated) { - $('#chat-user-status').attr('class', 'fa fa-circle status ' + data.status) - .attr('title', translated) - .attr('data-original-title', translated); - }); + socket.emit('user.checkStatus', chatModal.attr('touid'), function(err, status) { + if (err) { + return app.alertError(err.message); + } + updateStatus(status); }); } - function checkOnlineStatus(chatModal) { - if(parseInt(chatModal.attr('intervalId'), 10) === 0) { - chatModal.attr('intervalId', setInterval(function() { - checkStatus(chatModal); - }, 1000)); - } + function updateStatus(status) { + translator.translate('[[global:' + status + ']]', function(translated) { + $('#chat-user-status').attr('class', 'fa fa-circle status ' + status) + .attr('title', translated) + .attr('data-original-title', translated); + }); } module.createModal = function(username, touid, callback) { @@ -190,9 +193,10 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, var totalHeight = chatModal.find('.modal-content').outerHeight() - chatModal.find('.modal-header').outerHeight(); var padding = parseInt(chatModal.find('.modal-body').css('padding-top'), 10) + parseInt(chatModal.find('.modal-body').css('padding-bottom'), 10); var contentMargin = parseInt(chatModal.find('#chat-content').css('margin-top'), 10) + parseInt(chatModal.find('#chat-content').css('margin-bottom'), 10); + var sinceHeight = chatModal.find('.since-bar').outerHeight(true); var inputGroupHeight = chatModal.find('.input-group').outerHeight(); - chatModal.find('#chat-content').css('height', totalHeight - padding - contentMargin - inputGroupHeight); + chatModal.find('#chat-content').css('height', totalHeight - padding - contentMargin - sinceHeight - inputGroupHeight); }); chatModal.find('#chat-with-name').html(username); @@ -217,10 +221,18 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, } }); + chatModal.find('[data-since]').on('click', function() { + var since = $(this).attr('data-since'); + chatModal.find('[data-since]').removeClass('selected'); + $(this).addClass('selected'); + loadChatSince(chatModal, since); + return false; + }); + addSendHandler(chatModal); getChatMessages(chatModal, function() { - checkOnlineStatus(chatModal); + checkStatus(chatModal); }); chatModal.find('.user-typing .text').translateText('[[modules:chat.user_typing, ' + username + ']]'); @@ -250,7 +262,7 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, module.center = function(chatModal) { chatModal.css("left", Math.max(0, (($(window).width() - $(chatModal).outerWidth()) / 2) + $(window).scrollLeft()) + "px"); chatModal.css("top", Math.max(0, $(window).height() / 4 - $(chatModal).outerHeight() / 2)); - chatModal.css("zIndex", 2000); + chatModal.css("zIndex", 100); chatModal.find('#chat-message-input').focus(); return chatModal; }; @@ -258,7 +270,7 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, module.load = function(uuid) { var chatModal = $('div[UUID="'+uuid+'"]'); chatModal.removeClass('hide'); - checkOnlineStatus(chatModal); + checkStatus(chatModal); taskbar.updateActive(uuid); Chats.scrollToBottom(chatModal.find('#chat-content')); module.center(chatModal); @@ -276,7 +288,15 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, }; function getChatMessages(chatModal, callback) { - socket.emit('modules.chats.get', {touid: chatModal.attr('touid')}, function(err, messages) { + socket.emit('modules.chats.get', {touid: chatModal.attr('touid'), since: 'day'}, function(err, messages) { + module.appendChatMessage(chatModal, messages, callback); + }); + } + + function loadChatSince(chatModal, since, callback) { + socket.emit('modules.chats.get', {touid: chatModal.attr('touid'), since: since}, function(err, messages) { + var chatContent = chatModal.find('#chat-content'); + chatContent.find('.chat-message').remove(); module.appendChatMessage(chatModal, messages, callback); }); } @@ -290,11 +310,13 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, }); input.off('keyup').on('keyup', function() { - if ($(this).val()) { - socket.emit('modules.chats.userStartTyping', {touid:chatModal.attr('touid'), fromUid: app.uid}); - } else { - Chats.notifyTyping(chatModal.attr('touid'), false); + var val = !!$(this).val(); + if ((val && $(this).attr('data-typing') === 'true') || (!val && $(this).attr('data-typing') === 'false')) { + return; } + + Chats.notifyTyping(chatModal.attr('touid'), val); + $(this).attr('data-typing', val); }); chatModal.find('#chat-message-send-btn').off('click').on('click', function(e){ diff --git a/public/src/modules/composer.js b/public/src/modules/composer.js index 1ca81906de..90a9311987 100644 --- a/public/src/modules/composer.js +++ b/public/src/modules/composer.js @@ -9,11 +9,12 @@ var dependencies = [ 'composer/formatting', 'composer/drafts', 'composer/tags', + 'composer/categoryList', 'composer/preview', 'composer/resize' ]; -define('composer', dependencies, function(taskbar, controls, uploads, formatting, drafts, tags, preview, resize) { +define('composer', dependencies, function(taskbar, controls, uploads, formatting, drafts, tags, categoryList, preview, resize) { var composer = { active: undefined, posts: {}, @@ -127,9 +128,9 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting var prevText = bodyEl.val(); if (parseInt(tid, 10) !== parseInt(composer.posts[uuid].tid, 10)) { var link = '[' + title + '](/topic/' + topicSlug + '/' + (parseInt(postIndex, 10) + 1) + ')'; - translator.translate('[[modules:composer.user_said_in, ' + username + ', ' + link + ']]\n', onTranslated); + translator.translate('[[modules:composer.user_said_in, ' + username + ', ' + link + ']]\n', config.defaultLang, onTranslated); } else { - translator.translate('[[modules:composer.user_said, ' + username + ']]\n', onTranslated); + translator.translate('[[modules:composer.user_said, ' + username + ']]\n', config.defaultLang, onTranslated); } function onTranslated(translated) { @@ -141,7 +142,7 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting }; composer.newReply = function(tid, pid, title, text) { - translator.translate(text, function(translated) { + translator.translate(text, config.defaultLang, function(translated) { push({ tid: tid, toPid: pid, @@ -200,84 +201,96 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting var template = (composer.bsEnvironment === 'xs' || composer.bsEnvironment === 'sm') ? 'composer-mobile' : 'composer'; - templates.parse(template, {allowTopicsThumbnail: allowTopicsThumbnail, showTags: isTopic || isMain}, function(composerTemplate) { - translator.translate(composerTemplate, function(composerTemplate) { - composerTemplate = $(composerTemplate); + var data = { + allowTopicsThumbnail: allowTopicsThumbnail, + showTags: isTopic || isMain, + isTopic: isTopic + }; - composerTemplate.attr('id', 'cmp-uuid-' + post_uuid); + parseAndTranslate(template, data, function(composerTemplate) { - $(document.body).append(composerTemplate); + composerTemplate = $(composerTemplate); - var postContainer = $(composerTemplate[0]), - postData = composer.posts[post_uuid], - bodyEl = postContainer.find('textarea'), - draft = drafts.getDraft(postData.save_id); + composerTemplate.attr('id', 'cmp-uuid-' + post_uuid); - tags.init(postContainer, composer.posts[post_uuid]); - updateTitle(postData, postContainer); + $(document.body).append(composerTemplate); - activate(post_uuid); - resize.reposition(postContainer); + var postContainer = $(composerTemplate[0]), + postData = composer.posts[post_uuid], + bodyEl = postContainer.find('textarea'), + draft = drafts.getDraft(postData.save_id); - if (config.allowFileUploads || config.hasImageUploadPlugin) { - uploads.initialize(post_uuid); - } + tags.init(postContainer, composer.posts[post_uuid]); + categoryList.init(postContainer, composer.posts[post_uuid]); + updateTitle(postData, postContainer); - formatting.addHandler(postContainer); + activate(post_uuid); + resize.reposition(postContainer); - if (allowTopicsThumbnail) { - uploads.toggleThumbEls(postContainer, composer.posts[post_uuid].topic_thumb || ''); - } + if (config.allowFileUploads || config.hasImageUploadPlugin) { + uploads.initialize(post_uuid); + } - postContainer.on('change', 'input, textarea', function() { - composer.posts[post_uuid].modified = true; - }); + formatting.addHandler(postContainer); - postContainer.on('click', '.action-bar button[data-action="post"]', function() { - $(this).attr('disabled', true); - post(post_uuid); - }); + if (allowTopicsThumbnail) { + uploads.toggleThumbEls(postContainer, composer.posts[post_uuid].topic_thumb || ''); + } - postContainer.on('click', '.action-bar button[data-action="discard"]', function() { - if (!composer.posts[post_uuid].modified) { - discard(post_uuid); - return; - } + postContainer.on('change', 'input, textarea', function() { + composer.posts[post_uuid].modified = true; + }); - translator.translate('[[modules:composer.discard]]', function(translated) { - bootbox.confirm(translated, function(confirm) { - if (confirm) { - discard(post_uuid); - } - }); - }); - }); + postContainer.on('click', '.action-bar button[data-action="post"]', function() { + $(this).attr('disabled', true); + post(post_uuid); + }); - bodyEl.on('input propertychange', function() { - preview.render(postContainer); - }); + postContainer.on('click', '.action-bar button[data-action="discard"]', function() { + if (!composer.posts[post_uuid].modified) { + discard(post_uuid); + return; + } - bodyEl.on('scroll', function() { - preview.matchScroll(postContainer); + translator.translate('[[modules:composer.discard]]', function(translated) { + bootbox.confirm(translated, function(confirm) { + if (confirm) { + discard(post_uuid); + } + }); }); + }); - bodyEl.val(draft ? draft : postData.body); - preview.render(postContainer, function() { - preview.matchScroll(postContainer); - }); - drafts.init(postContainer, postData); + bodyEl.on('input propertychange', function() { + preview.render(postContainer); + }); + + bodyEl.on('scroll', function() { + preview.matchScroll(postContainer); + }); - resize.handleResize(postContainer); + bodyEl.val(draft ? draft : postData.body); + preview.render(postContainer, function() { + preview.matchScroll(postContainer); + }); + drafts.init(postContainer, postData); - handleHelp(postContainer); + resize.handleResize(postContainer); - $(window).trigger('action:composer.loaded', { - post_uuid: post_uuid - }); + handleHelp(postContainer); - formatting.addComposerButtons(); - focusElements(postContainer); + $(window).trigger('action:composer.loaded', { + post_uuid: post_uuid }); + + formatting.addComposerButtons(); + focusElements(postContainer); + }); + } + + function parseAndTranslate(template, data, callback) { + templates.parse(template, data, function(composerTemplate) { + translator.translate(composerTemplate, callback); }); } @@ -447,6 +460,7 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting $('body').css({'margin-bottom': 0}); $('.action-bar button').removeAttr('disabled'); + app.toggleNavbar(true); socket.emit('modules.composer.unregister', post_uuid); } } diff --git a/public/src/modules/composer/categoryList.js b/public/src/modules/composer/categoryList.js new file mode 100644 index 0000000000..776e435bc9 --- /dev/null +++ b/public/src/modules/composer/categoryList.js @@ -0,0 +1,37 @@ + +'use strict'; + +/*globals define, config, socket, app*/ + +define('composer/categoryList', function() { + var categoryList = {}; + + categoryList.init = function(postContainer, postData) { + var listEl = postContainer.find('.category-list'); + if (!listEl.length) { + return; + } + + socket.emit('categories.getCategoriesByPrivilege', 'topics:create', function(err, categories) { + if (err) { + return app.alertError(err.message); + } + + categories.forEach(function(category) { + $('').appendTo(listEl); + }); + + if (postData.cid) { + listEl.find('option[value="' + postData.cid + '"]').prop('selected', true); + } + }); + + listEl.on('change', function() { + if (postData.cid) { + postData.cid = this.value; + } + }); + }; + + return categoryList; +}); diff --git a/public/src/modules/composer/resize.js b/public/src/modules/composer/resize.js index a544aaf9b6..fb452b838a 100644 --- a/public/src/modules/composer/resize.js +++ b/public/src/modules/composer/resize.js @@ -22,7 +22,8 @@ define('composer/resize', function() { } if (env === 'sm' || env === 'xs') { - postContainer.css('height', $(window).height() - $('#header-menu').height()); + app.toggleNavbar(false); + postContainer.css('height', $(window).height()); } if (config.hasImageUploadPlugin) { @@ -137,8 +138,8 @@ define('composer/resize', function() { function resizeWritePreview(postContainer) { - var h1 = postContainer.find('.title').outerHeight(true); - var h2 = postContainer.find('.tags-container').outerHeight(true); + var h1 = postContainer.find('.title-container').outerHeight(true); + var h2 = postContainer.find('.category-tag-row').outerHeight(true); var h3 = postContainer.find('.formatting-bar').outerHeight(true); var h4 = postContainer.find('.topic-thumb-container').outerHeight(true); var h5 = $('.taskbar').height(); diff --git a/public/src/modules/composer/tags.js b/public/src/modules/composer/tags.js index 3930376680..93b33fbb38 100644 --- a/public/src/modules/composer/tags.js +++ b/public/src/modules/composer/tags.js @@ -33,8 +33,8 @@ define('composer/tags', function() { } if (tags) { response(tags); - $('.ui-autocomplete a').attr('href', '#'); } + $('.ui-autocomplete a').attr('data-ajaxify', 'false'); }); }, select: function(event, ui) { diff --git a/public/src/modules/composer/uploads.js b/public/src/modules/composer/uploads.js index 008970a3ca..54822aa681 100644 --- a/public/src/modules/composer/uploads.js +++ b/public/src/modules/composer/uploads.js @@ -235,10 +235,10 @@ define('composer/uploads', ['composer/preview'], function(preview) { textarea.val(current.replace(re, filename + '](' + text + ')')); } - $(this).find('#postUploadCsrf').val($('#csrf_token').val()); + $(this).find('#postUploadCsrf').val($('#csrf').attr('data-csrf')); if (formData) { - formData.append('_csrf', $('#csrf_token').val()); + formData.append('_csrf', $('#csrf').attr('data-csrf')); } uploads.inProgress[post_uuid] = uploads.inProgress[post_uuid] || []; @@ -291,7 +291,7 @@ define('composer/uploads', ['composer/preview'], function(preview) { thumbForm.attr('action', params.route); thumbForm.off('submit').submit(function() { - var csrf = $('#csrf_token').val(); + var csrf = $('#csrf').attr('data-csrf'); $(this).find('#thumbUploadCsrf').val(csrf); if(formData) { diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js index 88e3934128..0d62b52b00 100644 --- a/public/src/modules/navigator.js +++ b/public/src/modules/navigator.js @@ -11,8 +11,8 @@ define('navigator', ['forum/pagination'], function(pagination) { var count = 0; navigator.scrollActive = false; - navigator.init = function(selector, count, callback, toTop, toBottom) { - + navigator.init = function(selector, count, toTop, toBottom, callback, calculateIndex) { + index = 1; navigator.selector = selector; navigator.callback = callback; toTop = toTop || function() {}; @@ -24,6 +24,12 @@ define('navigator', ['forum/pagination'], function(pagination) { e.stopPropagation(); }); + $('.pagination-block').off('shown.bs.dropdown', '.dropdown').on('shown.bs.dropdown', '.dropdown', function() { + setTimeout(function() { + $('.pagination-block input').focus(); + }, 100); + }); + $('.pagination-block .pageup').off('click').on('click', navigator.scrollUp); $('.pagination-block .pagedown').off('click').on('click', navigator.scrollDown); $('.pagination-block .pagetop').off('click').on('click', toTop); @@ -36,7 +42,13 @@ define('navigator', ['forum/pagination'], function(pagination) { input.val(''); return; } - var url = generateUrl(input.val()); + + var index = parseInt(input.val(), 10); + if (typeof calculateIndex === 'function') { + index = calculateIndex(index, count); + } + + var url = generateUrl(index); input.val(''); $('.pagination-block .dropdown-toggle').trigger('click'); ajaxify.go(url); @@ -66,6 +78,11 @@ define('navigator', ['forum/pagination'], function(pagination) { }; function toggle(flag) { + var path = ajaxify.removeRelativePath(window.location.pathname.slice(1)); + if (flag && (!path.startsWith('topic') && !path.startsWith('category'))) { + return; + } + $('.pagination-block').toggleClass('hidden', !flag); } @@ -76,12 +93,9 @@ define('navigator', ['forum/pagination'], function(pagination) { var el = $(this); if (elementInView(el)) { - index = parseInt(el.attr('data-index'), 10) + 1; - - navigator.updateTextAndProgressBar(); - if (typeof navigator.callback === 'function') { - navigator.callback(el); + index = navigator.callback(el, count); + navigator.updateTextAndProgressBar(); } return false; @@ -147,7 +161,7 @@ define('navigator', ['forum/pagination'], function(pagination) { return scrollToPid(postIndex, highlight, duration, offset); } - if(config.usePagination) { + if (config.usePagination) { if (window.location.search.indexOf('page') !== -1) { navigator.update(); return; @@ -162,12 +176,19 @@ define('navigator', ['forum/pagination'], function(pagination) { } else { scrollToPid(postIndex, highlight, duration, offset); } + } else { + navigator.scrollActive = false; } }; function scrollToPid(postIndex, highlight, duration, offset) { var scrollTo = $('#post_anchor_' + postIndex); + if (!scrollTo) { + navigator.scrollActive = false; + return; + } + var done = false; function animateScroll() { $('html, body').animate({ @@ -195,7 +216,7 @@ define('navigator', ['forum/pagination'], function(pagination) { } } - if ($('#post-container').length && scrollTo.length) { + if ($('#post-container').length) { animateScroll(); } } diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index 750449fc72..95f7baacda 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -71,6 +71,11 @@ define('notifications', ['sounds'], function(sound) { Tinycon.setBubble(count); }; + function increaseNotifCount() { + var count = parseInt(notifIcon.attr('data-content'), 10) + 1; + updateNotifCount(count); + } + socket.emit('notifications.getCount', function(err, count) { if (!err) { updateNotifCount(count); @@ -79,7 +84,7 @@ define('notifications', ['sounds'], function(sound) { } }); - socket.on('event:new_notification', function(notifData, notifCount) { + socket.on('event:new_notification', function(notifData) { app.alert({ alert_id: 'new_notif', title: '[[notifications:new_notification]]', @@ -93,10 +98,11 @@ define('notifications', ['sounds'], function(sound) { ajaxify.refresh(); } - updateNotifCount(notifCount); + increaseNotifCount(); sound.play('notification'); }); + socket.on('event:notifications.updateCount', function(count) { updateNotifCount(count); }); diff --git a/public/src/modules/search.js b/public/src/modules/search.js new file mode 100644 index 0000000000..177e532c38 --- /dev/null +++ b/public/src/modules/search.js @@ -0,0 +1,93 @@ +define('search', ['navigator'], function(nav) { + "use strict"; + /* globals socket, ajaxify */ + + var Search = { + current: {} + }; + + Search.query = function(term, callback) { + try { + term = encodeURIComponent(term); + } catch(e) { + return app.alertError('[[error:invalid-search-term]]'); + } + + // Detect if a tid was specified + var topicSearch = term.match(/in:topic-([\d]+)/); + + if (!topicSearch) { + term = term.replace(/^[ ?#]*/, ''); + ajaxify.go('search/' + term); + callback(); + } else { + var cleanedTerm = term.replace(topicSearch[0], ''), + tid = topicSearch[1]; + + Search.queryTopic(tid, cleanedTerm, callback); + } + }; + + Search.queryTopic = function(tid, term, callback) { + socket.emit('topics.search', { + tid: tid, + term: term + }, function(err, pids) { + callback(err); + + // Sort pids numerically & store + Search.current = { + results: pids.sort(function(a, b) { + return a-b; + }), + tid: tid, + term: term + }; + + Search.topicDOM.update(0); + }); + }; + + Search.checkPagePresence = function(tid, callback) { + if (!ajaxify.currentPage.match(new RegExp('^topic/' + tid))) { + ajaxify.go('topic/' + tid, callback); + } else { + callback(); + } + }; + + Search.topicDOM = {}; + Search.topicDOM.prev = function() { + Search.topicDOM.update((Search.current.index === 0) ? Search.current.results.length-1 : Search.current.index-1); + }; + + Search.topicDOM.next = function() { + Search.topicDOM.update((Search.current.index === Search.current.results.length-1) ? 0 : Search.current.index+1); + }; + + Search.topicDOM.update = function(index) { + var topicSearchEl = $('.topic-search'); + Search.current.index = index; + + if (Search.current.results.length > 0) { + topicSearchEl.find('.count').html((index+1) + ' / ' + Search.current.results.length); + topicSearchEl.removeClass('hidden').find('.prev, .next').removeAttr('disabled'); + Search.checkPagePresence(Search.current.tid, function() { + socket.emit('posts.getPidIndex', Search.current.results[index], function(err, postIndex) { + nav.scrollToPost(postIndex-1, true); // why -1? Ask @barisusakli + }); + }); + } else { + translator.translate('[[search:no-matches]]', function(text) { + topicSearchEl.find('.count').html(text); + }); + topicSearchEl.removeClass('hidden').find('.prev, .next').attr('disabled', 'disabled'); + } + }; + + Search.topicDOM.end = function() { + $('.topic-search').addClass('hidden'); + }; + + return Search; +}); \ No newline at end of file diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index 29bc4a4c4e..78ac0ad044 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -448,17 +448,18 @@ define('settings', function () { helper.persistSettings(hash, Settings.cfg, notify, callback); }, load: function (hash, formEl, callback) { + callback = callback || function() {}; socket.emit('admin.settings.get', { hash: hash }, function (err, values) { - if (!err) { - $(formEl).deserialize(values); - if (typeof callback === 'function') { - callback(); - } - } else { + if (err) { console.log('[settings] Unable to load settings for hash: ', hash); + return callback(err); } + + $(formEl).deserialize(values); + + callback(null, values); }); }, save: function (hash, formEl, callback) { diff --git a/public/src/modules/templates.js b/public/src/modules/templates.js new file mode 100644 index 0000000000..2c25dbdb64 --- /dev/null +++ b/public/src/modules/templates.js @@ -0,0 +1,14 @@ +define('templates', function() { + var Templates = {}; + + Templates.refresh = function(callback) { + $.getJSON(RELATIVE_PATH + '/api/get_templates_listing', function (data) { + Templates.config = data.templatesConfig; + Templates.available = data.availableTemplates; + + if (callback) callback(); + }); + }; + + return Templates; +}); \ No newline at end of file diff --git a/public/src/modules/uploader.js b/public/src/modules/uploader.js index 64403f52fe..e44d59d4e2 100644 --- a/public/src/modules/uploader.js +++ b/public/src/modules/uploader.js @@ -19,6 +19,7 @@ define('uploader', function() { uploadForm[0].reset(); uploadForm.attr('action', route); uploadForm.find('#params').val(JSON.stringify(params)); + uploadForm.find('#csrfToken').val($('#csrf').attr('data-csrf')); if(fileSize) { uploadForm.find('#upload-file-size').html(fileSize); @@ -58,9 +59,6 @@ define('uploader', function() { return false; } - $(this).find('#imageUploadCsrf').val($('#csrf_token').val()); - - $(this).ajaxSubmit({ error: function(xhr) { xhr = maybeParse(xhr); diff --git a/public/src/translator.js b/public/src/translator.js index f81fff1ba8..cdb1e74926 100644 --- a/public/src/translator.js +++ b/public/src/translator.js @@ -21,10 +21,10 @@ translator.prepareDOM = function() { // Load the appropriate timeago locale file - if (config.defaultLang !== 'en_GB' && config.defaultLang !== 'en_US') { + if (config.userLang !== 'en_GB' && config.userLang !== 'en_US') { // Correct NodeBB language codes to timeago codes, if necessary var languageCode; - switch(config.defaultLang) { + switch(config.userLang) { case 'cs': languageCode = 'cz'; break; @@ -46,7 +46,7 @@ break; default: - languageCode = config.defaultLang; + languageCode = config.userLang; break; } @@ -69,7 +69,7 @@ if (typeof language === 'function') { callback = language; if ('undefined' !== typeof window && config) { - language = config.defaultLang || 'en_GB'; + language = config.userLang || 'en_GB'; } else { var meta = require('../../src/meta'); language = meta.config.defaultLang || 'en_GB'; diff --git a/public/src/utils.js b/public/src/utils.js index 28973b9c8d..104f8e4f6d 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -359,6 +359,16 @@ } }; + if (typeof String.prototype.startsWith != 'function') { + String.prototype.startsWith = function (prefix){ + if (this.length < prefix.length) + return false; + for (var i = prefix.length - 1; (i >= 0) && (this[i] === prefix[i]); --i) + continue; + return i < 0; + }; + } + if ('undefined' !== typeof window) { window.utils = module.exports; } diff --git a/public/src/widgets.js b/public/src/widgets.js index 23a37e4222..32b01f599f 100644 --- a/public/src/widgets.js +++ b/public/src/widgets.js @@ -30,60 +30,56 @@ ajaxify.widgets.reposition(); } - function renderWidgets(location) { - var area = $('#content [widget-area="' + location + '"]'), - areaData = { - location: location, - template: template + '.tpl', - url: url - }; - - $.get(RELATIVE_PATH + '/api/widgets/render', areaData, function(renderedWidgets) { - var html = ''; - - for (var i=0; i
    ')); - } else if (location === 'sidebar' && !$('#content [widget-area="sidebar"]').length) { - $('#content > *').wrapAll($('
    ')); - $('#content').append($('
    ')); - } else if (location === 'header' && !$('#content [widget-area="header"]').length) { - $('#content').prepend($('
    ')); + function renderWidgets(locations) { + var areaDatas = []; + + $.get(RELATIVE_PATH + '/api/widgets/render', { + locations: locations, + template: template + '.tpl', + url: url + }, function(renderedAreas) { + for (var x=0; x
    ')); + } else if (location === 'sidebar' && !$('#content [widget-area="sidebar"]').length) { + $('#content > *').wrapAll($('
    ')); + $('#content').append($('
    ')); + } else if (location === 'header' && !$('#content [widget-area="header"]').length) { + $('#content').prepend($('
    ')); + } - if (!renderedWidgets.length) { - area.addClass('hidden'); - ajaxify.widgets.reposition(location); - } + area = $('#content [widget-area="' + location + '"]'); + } - $('#content [widget-area] img:not(.user-img)').addClass('img-responsive'); - checkCallback(); - }); - } + area.html(html); - function checkCallback() { - numLocations--; - if (numLocations < 0) { + if (!renderedWidgets.length) { + area.addClass('hidden'); + ajaxify.widgets.reposition(location); + } + + $('#content [widget-area] img:not(.user-img)').addClass('img-responsive'); + } + $(window).trigger('action:widgets.loaded', {}); + if (typeof callback === 'function') { callback(); } - } - } - - for (var i=0; ig||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c 20) { - results.recentChats.length = 20; - } - if (!req.params.userslug) { return res.render('chats', { - chats: results.recentChats, + chats: results.recentChats.users, + nextStart: results.recentChats.nextStart, contacts: results.contacts }); } @@ -520,7 +535,7 @@ accountsController.getChats = function(req, res, next) { function(toUid, next) { async.parallel({ toUser: async.apply(user.getUserFields, toUid, ['uid', 'username']), - messages: async.apply(messaging.getMessages, req.user.uid, toUid, false) + messages: async.apply(messaging.getMessages, req.user.uid, toUid, 'day', false) }, next); } ], function(err, data) { @@ -529,7 +544,8 @@ accountsController.getChats = function(req, res, next) { } res.render('chats', { - chats: results.recentChats, + chats: results.recentChats.users, + nextStart: results.recentChats.nextStart, contacts: results.contacts, meta: data.toUser, messages: data.messages diff --git a/src/controllers/admin.js b/src/controllers/admin.js index cb81fab3d3..8ba54380d8 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -16,9 +16,9 @@ var async = require('async'), validator = require('validator'); - var adminController = { categories: {}, + tags: {}, topics: {}, groups: {}, themes: {}, @@ -38,6 +38,9 @@ adminController.home = function(req, res, next) { stats: function(next) { getStats(next); }, + pageviews: function(next) { + getPageviews(next); + }, notices: function(next) { var notices = [ {done: !meta.restartRequired, doneText: 'Restart not required', notDoneText:'Restart required'}, @@ -53,11 +56,24 @@ adminController.home = function(req, res, next) { res.render('admin/index', { version: pkg.version, notices: results.notices, - stats: results.stats + stats: results.stats, + pageviews: results.pageviews }); }); }; +function getPageviews(callback) { + async.parallel({ + monthly: function(next) { + db.get('pageviews:monthly', next); + }, + daily: function(next) { + db.get('pageviews:daily', next); + } + }, function(err, results) { + callback(null, results); + }); +} function getStats(callback) { async.parallel([ function(next) { @@ -117,15 +133,36 @@ adminController.categories.disabled = function(req, res, next) { }; function filterAndRenderCategories(req, res, next, active) { - categories.getAllCategories(function (err, categoryData) { + var uid = req.user ? parseInt(req.user.uid, 10) : 0; + categories.getAllCategories(uid, function (err, categoryData) { + if (err) { + return next(err); + } + categoryData = categoryData.filter(function (category) { + if (!category) { + return false; + } return active ? !category.disabled : category.disabled; }); - res.render('admin/categories', {categories: categoryData}); + res.render('admin/categories', { + categories: categoryData, + csrf: req.csrfToken() + }); }); } +adminController.tags.get = function(req, res, next) { + topics.getTags(0, 99, function(err, tags) { + if (err) { + return next(err); + } + + res.render('admin/tags', {tags: tags}); + }); +}; + adminController.database.get = function(req, res, next) { db.info(function (err, data) { res.render('admin/database', data); @@ -166,7 +203,9 @@ adminController.languages.get = function(req, res, next) { }; adminController.settings.get = function(req, res, next) { - res.render('admin/settings', {}); + res.render('admin/settings', { + 'csrf': req.csrfToken() + }); }; adminController.logger.get = function(req, res, next) { diff --git a/src/controllers/api.js b/src/controllers/api.js index 7944fa45f2..a43678d22a 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -30,6 +30,7 @@ apiController.getConfig = function(req, res, next) { config.allowGuestSearching = parseInt(meta.config.allowGuestSearching, 10) === 1; config.allowFileUploads = parseInt(meta.config.allowFileUploads, 10) === 1; config.allowTopicsThumbnail = parseInt(meta.config.allowTopicsThumbnail, 10) === 1; + config.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1; config.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1; config.usePagination = parseInt(meta.config.usePagination, 10) === 1; config.disableSocialButtons = parseInt(meta.config.disableSocialButtons, 10) === 1; @@ -42,6 +43,7 @@ apiController.getConfig = function(req, res, next) { config.maximumFileSize = meta.config.maximumFileSize; config['theme:id'] = meta.config['theme:id']; config.defaultLang = meta.config.defaultLang || 'en_GB'; + config.userLang = config.defaultLang; config.environment = process.env.NODE_ENV; config.loggedIn = !!req.user; config['cache-buster'] = meta.config['cache-buster'] || ''; @@ -68,7 +70,7 @@ apiController.getConfig = function(req, res, next) { config.topicsPerPage = settings.topicsPerPage; config.postsPerPage = settings.postsPerPage; config.notificationSounds = settings.notificationSounds; - config.defaultLang = settings.language || config.defaultLang; + config.userLang = settings.language || config.defaultLang; config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; config.topicPostSort = settings.topicPostSort || config.topicPostSort; @@ -83,19 +85,28 @@ apiController.getConfig = function(req, res, next) { apiController.renderWidgets = function(req, res, next) { - var uid = req.user ? req.user.uid : 0, - area = { + var async = require('async'), + uid = req.user ? req.user.uid : 0, + areas = { template: req.query.template, - location: req.query.location, + locations: req.query.locations, url: req.query.url - }; + }, + renderedWidgets = []; - if (!area.template || !area.location) { + if (!areas.template || !areas.locations) { return res.json(200, {}); } - widgets.render(uid, area, function(err, data) { - res.json(200, data); + widgets.render(uid, { + template: areas.template, + url: areas.url, + locations: areas.locations + }, function(err, widgets) { + if (err) { + return next(err); + } + res.json(200, widgets); }); }; diff --git a/src/controllers/categories.js b/src/controllers/categories.js index d11567c4de..d3b4291cd8 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -5,20 +5,21 @@ var categoriesController = {}, qs = require('querystring'), nconf = require('nconf'), privileges = require('../privileges'), - user = require('./../user'), - categories = require('./../categories'), - topics = require('./../topics'), - meta = require('./../meta'), - plugins = require('./../plugins'); + user = require('../user'), + categories = require('../categories'), + topics = require('../topics'), + meta = require('../meta'), + plugins = require('../plugins'); categoriesController.recent = function(req, res, next) { var uid = req.user ? req.user.uid : 0; - topics.getLatestTopics(uid, 0, 19, req.params.term, function (err, data) { - if(err) { + var end = (parseInt(meta.config.topicsPerList, 10) || 20) - 1; + topics.getLatestTopics(uid, 0, end, req.params.term, function (err, data) { + if (err) { return next(err); } - data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] === '1' ? true : false; + data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; plugins.fireHook('filter:category.get', data, uid, function(err, data) { res.render('recent', data); @@ -26,19 +27,32 @@ categoriesController.recent = function(req, res, next) { }); }; +var anonCache = {}, lastUpdateTime = 0; + categoriesController.popular = function(req, res, next) { var uid = req.user ? req.user.uid : 0; var term = req.params.term || 'daily'; - topics.getPopular(term, uid, function(err, data) { - if(err) { + if (uid === 0) { + if (anonCache[term] && (Date.now() - lastUpdateTime) < 60 * 60 * 1000) { + return res.render('popular', anonCache[term]); + } + } + + topics.getPopular(term, uid, meta.config.topicsPerList, function(err, data) { + if (err) { return next(err); } - data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] === '1' ? true : false; + data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; plugins.fireHook('filter:category.get', {topics: data}, uid, function(err, data) { + if (uid === 0) { + anonCache[term] = data; + lastUpdateTime = Date.now(); + } + res.render('popular', data); }); }); @@ -46,9 +60,9 @@ categoriesController.popular = function(req, res, next) { categoriesController.unread = function(req, res, next) { var uid = req.user ? req.user.uid : 0; - - topics.getUnreadTopics(uid, 0, 20, function (err, data) { - if(err) { + var end = (parseInt(meta.config.topicsPerList, 10) || 20) - 1; + topics.getUnreadTopics(uid, 0, end, function (err, data) { + if (err) { return next(err); } @@ -77,43 +91,54 @@ categoriesController.get = function(req, res, next) { async.waterfall([ function(next) { - categories.getCategoryField(cid, 'disabled', next); + async.parallel({ + exists: function(next) { + categories.exists(cid, next); + }, + disabled: function(next) { + categories.getCategoryField(cid, 'disabled', next); + }, + privileges: function(next) { + privileges.categories.get(cid, uid, next); + }, + userSettings: function(next) { + user.getSettings(uid, next); + } + }, next); }, - function(disabled, next) { - if (parseInt(disabled, 10) === 1) { - return next(new Error('[[error:category-disabled]]')); + function(results, next) { + if (!results.exists || parseInt(results.disabled, 10) === 1) { + return categoriesController.notFound(req, res); } - privileges.categories.get(cid, uid, next); - }, - function (privileges, next) { - if (!privileges.read) { - return next(new Error('[[error:no-privileges]]')); + if (!results.privileges.read) { + return categoriesController.notAllowed(req, res); } - user.getSettings(uid, function(err, settings) { + var settings = results.userSettings; + + var topicIndex = 0; + if (!settings.usePagination) { + topicIndex = Math.max((req.params.topic_index || 1) - (settings.topicsPerPage - 1), 0); + } else if (!req.query.page) { + var index = Math.max(parseInt((req.params.topic_index || 0), 10), 0); + page = Math.ceil((index + 1) / settings.topicsPerPage); + } + + var start = (page - 1) * settings.topicsPerPage + topicIndex, + end = start + settings.topicsPerPage - 1; + + categories.getCategoryById(cid, start, end, uid, function (err, categoryData) { if (err) { return next(err); } - var topicIndex = 0; - if (!settings.usePagination) { - topicIndex = Math.max((req.params.topic_index || 1) - (settings.topicsPerPage - 1), 0); - } else if (!req.query.page) { - var index = Math.max(parseInt((req.params.topic_index || 0), 10), 0); - page = Math.ceil((index + 1) / settings.topicsPerPage); - } - - var start = (page - 1) * settings.topicsPerPage + topicIndex, - end = start + settings.topicsPerPage - 1; - - categories.getCategoryById(cid, start, end, uid, function (err, categoryData) { + categories.getRecentTopicReplies(categoryData.children, uid, function(err) { if (err) { return next(err); } - - categoryData.privileges = privileges; - next(err, categoryData); + categoryData.privileges = results.privileges; + next(null, categoryData); }); }); }, @@ -160,7 +185,7 @@ categoriesController.get = function(req, res, next) { } ], function (err, data) { if (err) { - return res.locals.isAPI ? res.json(404, 'not-found') : res.redirect(nconf.get('relative_path') + '/404'); + return next(err); } if (data.link) { @@ -174,18 +199,40 @@ categoriesController.get = function(req, res, next) { } data.currentPage = page; - data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] === '1' ? true : false; - - // Paginator for noscript - data.pages = []; - for(var x=1;x<=data.pageCount;x++) { - data.pages.push({ - page: x, - active: x === parseInt(page, 10) - }); + data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + data.csrf = req.csrfToken(); + + if (!res.locals.isAPI) { + // Paginator for noscript + data.pages = []; + for(var x=1;x<=data.pageCount;x++) { + data.pages.push({ + page: x, + active: x === parseInt(page, 10) + }); + } } + res.render('category', data); }); }; +categoriesController.notFound = function(req, res) { + res.locals.isAPI ? res.json(404, 'not-found') : res.redirect(nconf.get('relative_path') + '/404'); +}; + +categoriesController.notAllowed = function(req, res) { + var uid = req.user ? req.user.uid : 0; + if (uid) { + res.locals.isAPI ? res.json(403, 'not-allowed') : res.redirect(nconf.get('relative_path') + '/403'); + } else { + if (res.locals.isAPI) { + res.json(401, 'not-authorized'); + } else { + req.session.returnTo = req.url; + res.redirect(nconf.get('relative_path') + '/login'); + } + } +}; + module.exports = categoriesController; diff --git a/src/controllers/groups.js b/src/controllers/groups.js index a7295bdd7e..9185e6e1aa 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -17,6 +17,7 @@ groupsController.list = function(req, res) { }; groupsController.details = function(req, res) { + var uid = req.user ? parseInt(req.user.uid, 10) : 0; async.parallel({ group: function(next) { groups.get(req.params.name, { @@ -24,7 +25,7 @@ groupsController.details = function(req, res) { }, next); }, posts: function(next) { - groups.getLatestMemberPosts(req.params.name, 10, next); + groups.getLatestMemberPosts(req.params.name, 10, uid, next); } }, function(err, results) { if (!err) { diff --git a/src/controllers/index.js b/src/controllers/index.js index 96748917b6..0dabd3dcab 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -68,18 +68,27 @@ Controllers.home = function(req, res, next) { if (err) { return next(err); } + var childCategories = []; - function getRecentReplies(category, callback) { - categories.getRecentTopicReplies(category.cid, uid, parseInt(category.numRecentReplies, 10), function (err, posts) { - if (err) { - return callback(err); - } - category.posts = posts; - callback(); - }); + for(var i=categoryData.length - 1; i>=0; --i) { + + if (Array.isArray(categoryData[i].children) && categoryData[i].children.length) { + childCategories.push.apply(childCategories, categoryData[i].children); + } + + if (categoryData[i].parent && categoryData[i].parent.cid) { + categoryData.splice(i, 1); + } } - async.each(categoryData, getRecentReplies, function (err) { + async.parallel([ + function(next) { + categories.getRecentTopicReplies(categoryData, uid, next); + }, + function(next) { + categories.getRecentTopicReplies(childCategories, uid, next); + } + ], function(err) { next(err, categoryData); }); }); @@ -108,6 +117,8 @@ Controllers.search = function(req, res, next) { return res.redirect('/404'); } + req.params.term = req.params.term.replace(/"/g, '/"'); + search.search(req.params.term, uid, function(err, results) { if (err) { return next(err); @@ -131,14 +142,11 @@ Controllers.login = function(req, res, next) { data.alternate_logins = num_strategies > 0; data.authentication = login_strategies; - data.token = res.locals.csrf_token; + data.token = req.csrfToken(); data.showResetLink = emailersPresent; data.allowLocalLogin = meta.config.allowLocalLogin === undefined || parseInt(meta.config.allowLocalLogin, 10) === 1; data.allowRegistration = meta.config.allowRegistration; - - if (req.query.next) { - data.previousUrl = req.query.next; - } + data.error = req.flash('error')[0]; res.render('login', data); }; @@ -163,7 +171,7 @@ Controllers.register = function(req, res, next) { data.authentication = login_strategies; - data.token = res.locals.csrf_token; + data.token = req.csrfToken(); data.minimumUsernameLength = meta.config.minimumUsernameLength; data.maximumUsernameLength = meta.config.maximumUsernameLength; data.minimumPasswordLength = meta.config.minimumPasswordLength; @@ -188,7 +196,7 @@ Controllers.confirmEmail = function(req, res, next) { Controllers.sitemap = function(req, res, next) { if (meta.config['feeds:disableSitemap'] === '1') { - return res.redirect(nconf.get('relative_path') + '/404') + return res.redirect(nconf.get('relative_path') + '/404'); } var sitemap = require('../sitemap.js'); diff --git a/src/controllers/tags.js b/src/controllers/tags.js index 74c21a17fa..755ede4b91 100644 --- a/src/controllers/tags.js +++ b/src/controllers/tags.js @@ -3,13 +3,15 @@ var tagsController = {}, async = require('async'), nconf = require('nconf'), - topics = require('./../topics'); + validator = require('validator'), + meta = require('../meta'), + topics = require('../topics'); tagsController.getTag = function(req, res, next) { - var tag = req.params.tag; + var tag = validator.escape(req.params.tag); var uid = req.user ? req.user.uid : 0; - - topics.getTagTids(tag, 0, 19, function(err, tids) { + var end = (parseInt(meta.config.topicsPerList, 10) || 20) - 1; + topics.getTagTids(tag, 0, end, function(err, tids) { if (err) { return next(err); } @@ -46,14 +48,13 @@ tagsController.getTag = function(req, res, next) { }; tagsController.getTags = function(req, res, next) { - topics.getTags(0, -1, function(err, tags) { + topics.getTags(0, 99, function(err, tags) { if (err) { return next(err); } - res.render('tags', {tags: tags}); + res.render('tags', {tags: tags, nextStart: 100}); }); - }; module.exports = tagsController; diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 51d1706885..b925568127 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -6,65 +6,117 @@ var topicsController = {}, validator = require('validator'), nconf = require('nconf'), qs = require('querystring'), - user = require('./../user'), - meta = require('./../meta'), - topics = require('./../topics'), + user = require('../user'), + meta = require('../meta'), + topics = require('../topics'), posts = require('../posts'), privileges = require('../privileges'), - utils = require('./../../public/src/utils'); + categoriesController = require('./categories'), + utils = require('../../public/src/utils'); topicsController.get = function(req, res, next) { var tid = req.params.topic_id, - page = req.query.page || 1, + page = 1, + sort = req.query.sort, uid = req.user ? req.user.uid : 0, userPrivileges; async.waterfall([ function (next) { - privileges.topics.get(tid, uid, next); + async.parallel({ + privileges: function(next) { + privileges.topics.get(tid, uid, next); + }, + postCount: function(next) { + topics.getPostCount(tid, next); + }, + settings: function(next) { + user.getSettings(uid, next); + } + }, next); }, - function (privileges, next) { - if (!privileges.read || privileges.disabled) { - return next(new Error('[[error:no-privileges]]')); + function (results, next) { + + var postCount = parseInt(results.postCount, 10) + 1; + if (utils.isNumber(req.params.post_index)) { + var url = ''; + if (req.params.post_index > postCount) { + url = '/topic/' + req.params.topic_id + '/' + req.params.slug + '/' + postCount; + return res.locals.isAPI ? res.json(302, url) : res.redirect(url); + } else if (req.params.post_index < 1) { + url = '/topic/' + req.params.topic_id + '/' + req.params.slug; + return res.locals.isAPI ? res.json(302, url) : res.redirect(url); + } } - userPrivileges = privileges; + userPrivileges = results.privileges; - user.getSettings(uid, next); - }, - function (settings, next) { - var postIndex = 0; - if (!settings.usePagination) { - postIndex = Math.max((req.params.post_index || 1) - (settings.postsPerPage - 1), 0); - } else if (!req.query.page) { - var index = Math.max(parseInt((req.params.post_index || 0), 10), 0); - page = Math.ceil((index + 1) / settings.postsPerPage); + if (userPrivileges.disabled) { + return categoriesController.notFound(req, res); } - var start = (page - 1) * settings.postsPerPage + postIndex, - end = start + settings.postsPerPage - 1; + if (!userPrivileges.read) { + return categoriesController.notAllowed(req, res); + } + var settings = results.settings; var set = 'tid:' + tid + ':posts', reverse = false; - if (settings.topicPostSort === 'newest_to_oldest') { + // `sort` qs has priority over user setting + if (sort === 'oldest_to_newest') { + reverse = false; + } else if (sort === 'newest_to_oldest') { + reverse = true; + } else if (sort === 'most_votes') { + reverse = true; + set = 'tid:' + tid + ':posts:votes'; + } else 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 (topicData.deleted && !userPrivileges.view_deleted) { - return next(new Error('[[error:no-topic]]')); - } - topicData.currentPage = page; - if(page > 1) { - topicData.posts.splice(0, 1); + var postIndex = 0; + page = parseInt(req.query.page, 10) || 1; + req.params.post_index = parseInt(req.params.post_index, 10) || 0; + if (!settings.usePagination) { + if (reverse) { + if (req.params.post_index === 1) { + req.params.post_index = 0; } + postIndex = Math.max(postCount - (req.params.post_index || postCount) - (settings.postsPerPage - 1), 0); + } else { + postIndex = Math.max((req.params.post_index || 1) - (settings.postsPerPage + 1), 0); } - next(err, topicData); + } else if (!req.query.page) { + var index = Math.max(req.params.post_index - 1, 0) || 0; + page = Math.max(1, Math.ceil(index / settings.postsPerPage)); + } + + var start = (page - 1) * settings.postsPerPage + postIndex, + end = start + settings.postsPerPage - 1; + + topics.getTopicWithPosts(tid, set, uid, start, end, reverse, function (err, topicData) { + if (err && err.message === '[[error:no-topic]]' && !topicData) { + return categoriesController.notFound(req, res); + } + if (err && !topicData) { + return next(err); + } + if (topicData.deleted && !userPrivileges.view_deleted) { + return categoriesController.notAllowed(req, res); + } + + topicData.pageCount = Math.ceil(parseInt(postCount, 10) / settings.postsPerPage); + + topicData.currentPage = page; + if(page > 1) { + topicData.posts.splice(0, 1); + } + + next(null, topicData); }); }, function (topicData, next) { @@ -140,7 +192,7 @@ topicsController.get = function(req, res, next) { }, { property: 'article:section', - content: topicData.category.name + content: topicData.category ? topicData.category.name : '' } ]; @@ -150,26 +202,31 @@ topicsController.get = function(req, res, next) { type: 'application/rss+xml', href: nconf.get('url') + '/topic/' + tid + '.rss' }, - { - rel: 'up', - href: nconf.get('url') + '/category/' + topicData.category.slug - }, { rel: 'canonical', href: nconf.get('url') + '/topic/' + topicData.slug } ]; + if (topicData.category) { + res.locals.linkTags.push({ + rel: 'up', + href: nconf.get('url') + '/category/' + topicData.category.slug + }); + } + next(null, topicData); } ], function (err, data) { if (err) { - return res.locals.isAPI ? res.json(404, 'not-found') : res.redirect(nconf.get('relative_path') + '/404'); + return next(err); } data.privileges = userPrivileges; - data['reputation:disabled'] = meta.config['reputation:disabled'] === '1' ? true : false; - data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] === '1' ? true : false; + data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; + data['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; + data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + data.csrf = req.csrfToken(); var topic_url = tid + (req.params.slug ? '/' + req.params.slug : ''); var queryString = qs.stringify(req.query); @@ -177,39 +234,58 @@ topicsController.get = function(req, res, next) { topic_url += '?' + queryString; } - // Paginator for noscript - data.pages = []; - for(var x=1; x<=data.pageCount; x++) { - data.pages.push({ - page: x, - active: x === parseInt(page, 10) - }); + if (!res.locals.isAPI) { + // Paginator for noscript + data.pages = []; + for(var x=1; x<=data.pageCount; x++) { + data.pages.push({ + page: x, + active: x === parseInt(page, 10) + }); + } } + res.render('topic', data); }); }; topicsController.teaser = function(req, res, next) { var tid = req.params.topic_id; - topics.getLatestUndeletedPid(tid, function(err, pid) { + var uid = req.user ? parseInt(req.user.uid, 10) : 0; + + if (!utils.isNumber(tid)) { + return next(new Error('[[error:invalid-tid]]')); + } + + privileges.topics.can('read', tid, uid, function(err, canRead) { if (err) { return next(err); } - if (!pid) { - return res.json(404, 'not-found'); + if (!canRead) { + return res.json(403, '[[error:no-priveges]]'); } - posts.getPostSummaryByPids([pid], {stripTags: false}, function(err, posts) { + topics.getLatestUndeletedPid(tid, function(err, pid) { if (err) { return next(err); } - if (!Array.isArray(posts) || !posts.length) { + if (!pid) { return res.json(404, 'not-found'); } - res.json(posts[0]); + posts.getPostSummaryByPids([pid], uid, {stripTags: false}, function(err, posts) { + if (err) { + return next(err); + } + + if (!Array.isArray(posts) || !posts.length) { + return res.json(404, 'not-found'); + } + + res.json(posts[0]); + }); }); }); }; diff --git a/src/controllers/users.js b/src/controllers/users.js index d40f5acd7f..039bb4468f 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -3,65 +3,45 @@ var usersController = {}; var async = require('async'), - user = require('./../user'), - db = require('./../database'); + user = require('../user'), + db = require('../database'); usersController.getOnlineUsers = function(req, res, next) { var websockets = require('../socket.io'); - - user.getUsersFromSet('users:online', 0, 49, function (err, users) { - if(err) { + var uid = req.user ? req.user.uid : 0; + + async.parallel({ + users: function(next) { + user.getUsersFromSet('users:online', 0, 49, next); + }, + count: function(next) { + db.sortedSetCard('users:online', next); + }, + isAdministrator: function(next) { + user.isAdministrator(uid, next); + } + }, function(err, results) { + if (err) { return next(err); } - var onlineUsers = [], - uid = req.user ? req.user.uid : 0; - - user.isAdministrator(uid, function (err, isAdministrator) { - if(err) { - return next(err); - } - - if (!isAdministrator) { - users = users.filter(function(user) { - return user.status !== 'offline'; - }); - } - - function updateUserOnlineStatus(user, next) { - var online = websockets.isUserOnline(user.uid); - if (!online) { - db.sortedSetRemove('users:online', user.uid); - return next(); - } - - onlineUsers.push(user); - next(); - } - - var anonymousUserCount = websockets.getOnlineAnonCount(); - - async.each(users, updateUserOnlineStatus, function(err) { - if (err) { - return next(err); - } - - db.sortedSetCard('users:online', function(err, count) { - if (err) { - return next(err); - } - - var userData = { - search_display: 'none', - loadmore_display: count > 50 ? 'block' : 'hide', - users: onlineUsers, - anonymousUserCount: anonymousUserCount, - show_anon: anonymousUserCount?'':'hide' - }; - - res.render('users', userData); - }); + + if (!results.isAdministrator) { + results.users = results.users.filter(function(user) { + return user && user.status !== 'offline'; }); - }); + } + + var anonymousUserCount = websockets.getOnlineAnonCount(); + + var userData = { + search_display: 'none', + loadmore_display: results.count > 50 ? 'block' : 'hide', + users: results.users, + anonymousUserCount: anonymousUserCount, + show_anon: anonymousUserCount ? '' : 'hide' + }; + + res.render('users', userData); }); }; @@ -78,25 +58,29 @@ usersController.getUsersSortedByJoinDate = function(req, res, next) { }; function getUsers(set, res, next) { - user.getUsersFromSet(set, 0, 49, function (err, data) { + async.parallel({ + users: function(next) { + user.getUsersFromSet(set, 0, 49, next); + }, + count: function(next) { + db.sortedSetCard(set, next); + } + }, function(err, results) { if (err) { return next(err); } + results.users = results.users.filter(function(user) { + return user && parseInt(user.uid, 10); + }); - db.sortedSetCard(set, function(err, count) { - if (err) { - return next(err); - } - - var userData = { - search_display: 'none', - loadmore_display: count > 50 ? 'block' : 'hide', - users: data, - show_anon: 'hide' - }; + var userData = { + search_display: 'none', + loadmore_display: results.count > 50 ? 'block' : 'hide', + users: results.users, + show_anon: 'hide' + }; - res.render('users', userData); - }); + res.render('users', userData); }); } diff --git a/src/database/level/hash.js b/src/database/level/hash.js index c321fc9dae..b35a1b1193 100644 --- a/src/database/level/hash.js +++ b/src/database/level/hash.js @@ -18,7 +18,7 @@ module.exports = function(db, module) { } ], function(err) { if (typeof callback === 'function') { - callback(err, obj); + callback(err); } }); }; @@ -42,7 +42,7 @@ module.exports = function(db, module) { if (typeof callback === 'function') { callback(err, obj); } - }); + }); } else { if (typeof callback === 'function') { callback(err, {}); diff --git a/src/database/level/list.js b/src/database/level/list.js index 28a899f001..ca579178f0 100644 --- a/src/database/level/list.js +++ b/src/database/level/list.js @@ -2,7 +2,7 @@ module.exports = function(db, module) { var helpers = module.helpers.level; - + module.listPrepend = function(key, value, callback) { module.getListRange(key, 0, -1, function(err, list) { var arr = list || []; @@ -17,7 +17,7 @@ module.exports = function(db, module) { arr.push(value); module.set(key, arr, function(err) { if (typeof callback === 'function') { - callback(err, list); + callback(err); } }); }); diff --git a/src/database/level/main.js b/src/database/level/main.js index 97586c33b2..00c55bb16d 100644 --- a/src/database/level/main.js +++ b/src/database/level/main.js @@ -41,6 +41,10 @@ module.exports = function(db, module) { db.del(key, callback); }; + module.deleteAll = function(keys, callback) { + async.each(keys, module.delete, callback); + }; + module.get = function(key, callback) { db.get(key, function(err, value) { callback(false, value); diff --git a/src/database/level/sets.js b/src/database/level/sets.js index 5d6ffabf6c..b7697f1209 100644 --- a/src/database/level/sets.js +++ b/src/database/level/sets.js @@ -6,23 +6,33 @@ module.exports = function(db, module) { var helpers = module.helpers.level; module.setAdd = function(key, value, callback) { + callback = callback || function() {}; module.getListRange(key, 0, -1, function(err, set) { + if (err) { + return callback(err); + } if (set.indexOf(value) === -1) { module.listAppend(key, value, callback); } else { - if (typeof callback === 'function') { - callback(null, []); // verify if it sends back true on redis? - } + callback(null); } }); }; + module.setsAdd = function(keys, value, callback) { + throw new Error('not-implemented'); + }; + module.setRemove = function(key, value, callback) { module.getListRange(key, 0, -1, function(err, set) { module.set(key, set.splice(set.indexOf(value), 1), callback); }); }; + module.setsRemove = function(keys, value, callback) { + throw new Error('not-implemented'); + }; + module.isSetMember = function(key, value, callback) { module.getListRange(key, 0, -1, function(err, set) { callback(err, set.indexOf(value) !== -1); diff --git a/src/database/level/sorted.js b/src/database/level/sorted.js index 623774aac2..d262af89c2 100644 --- a/src/database/level/sorted.js +++ b/src/database/level/sorted.js @@ -7,6 +7,9 @@ module.exports = function(db, module) { var helpers = module.helpers.level; module.sortedSetAdd = function(key, score, value, callback) { + if (Array.isArray(score) && Array.isArray(value)) { + return sortedSetAddMulti(key, score, value, callback); + } module.getListRange(key, 0, -1, function(err, set) { set = set.filter(function(a) {return a.value !== value.toString();}); @@ -20,9 +23,24 @@ module.exports = function(db, module) { }); }; + function sortedSetAddMulti(key, scores, values, callback) { + throw new Error('not implemented'); + } + + module.sortedSetsAdd = function(keys, score, value, callback) { + async.each(keys, function(key, next) { + module.sortedSetAdd(key, score, value, next); + }, function(err) { + callback(err); + }); + }; + module.sortedSetRemove = function(key, value, callback) { + if (!Array.isArray(value)) { + value = [value]; + } module.getListRange(key, 0, -1, function(err, set) { - set = set.filter(function(a) {return a.value !== value.toString();}); + set = set.filter(function(a) { return value.indexOf(a) === -1;}); module.set(key, set, callback); }); }; @@ -33,6 +51,10 @@ module.exports = function(db, module) { }, callback); }; + module.sortedSetsRemoveRangeByScore = function(keys, min, max, callback) { + throw new Error('not implemented'); + }; + function flattenSortedSet(set, callback) { callback(null, !set.length ? [] : set.reduce(function(a, b) { return (a.length ? a : [a.value]).concat([b.value]); @@ -113,6 +135,10 @@ module.exports = function(db, module) { }); }; + module.sortedSetsCard = function(keys, callback) { + async.map(keys, module.sortedSetCard, callback); + }; + module.sortedSetRank = function(key, value, callback) { module.getListRange(key, 0, -1, function(err, list) { for (var i = 0, ii=list.length; i< ii; i++) { @@ -149,6 +175,35 @@ module.exports = function(db, module) { }); }; + module.sortedSetScores = function(key, values, callback) { + values = values.map(function(value) { + return value ? value.toString() : value; + }); + + module.getListRange(key, 0, -1, function(err, list) { + if (err) { + return callback(err); + } + + var map = {}; + list = list.filter(function(item) { + return values.indexOf(item.value) !== -1; + }).forEach(function(item) { + map[item.value] = item.score; + }); + + var returnData = new Array(values.length), + score; + + for(var i=0; i=0) { - /* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */ - redisClient = redis.createClient(nconf.get('redis:host')); - } else { - /* Else, connect over tcp/ip */ - redisClient = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host')); - } - - if (nconf.get('redis:password')) { - redisClient.auth(nconf.get('redis:password')); - } else { - winston.warn('You have no redis password setup!'); - } - - redisClient.on('error', function (err) { - winston.error(err.stack); - process.exit(1); - }); + redisClient = module.connect(); module.client = redisClient; @@ -104,6 +85,34 @@ } }; + module.connect = function() { + var redis_socket_or_host = nconf.get('redis:host'), + cxn; + + if (!redis) redis = require('redis'); + + if (redis_socket_or_host && redis_socket_or_host.indexOf('/') >= 0) { + /* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */ + cxn = redis.createClient(nconf.get('redis:host')); + } else { + /* Else, connect over tcp/ip */ + cxn = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host')); + } + + if (nconf.get('redis:password')) { + cxn.auth(nconf.get('redis:password')); + } else { + winston.warn('You have no redis password setup!'); + } + + cxn.on('error', function (err) { + winston.error(err.stack); + process.exit(1); + }); + + return cxn; + }; + module.close = function() { redisClient.quit(); }; diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index 63a85f77ec..b4fb033348 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -3,7 +3,9 @@ module.exports = function(redisClient, module) { module.setObject = function(key, data, callback) { callback = callback || function() {}; - redisClient.hmset(key, data, callback); + redisClient.hmset(key, data, function(err) { + callback(err); + }); }; module.setObjectField = function(key, field, value, callback) { diff --git a/src/database/redis/main.js b/src/database/redis/main.js index 1cf8d35f5c..986c3a7bc8 100644 --- a/src/database/redis/main.js +++ b/src/database/redis/main.js @@ -73,6 +73,14 @@ module.exports = function(redisClient, module) { redisClient.del(key, callback); }; + module.deleteAll = function(keys, callback) { + var multi = redisClient.multi(); + for(var i=0; i[ \t]*/; + + while(matches = file.match(regex)) { + var partial = "/" + matches[1]; + + if (paths[partial] && relativePath !== partial) { + file = file.replace(regex, fs.readFileSync(paths[partial]).toString()); + } else { + winston.warn('[themes] Partial not loaded: ' + matches[1]); + file = file.replace(regex, ""); + } + } + + mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/'))); + fs.writeFile(path.join(viewsPath, relativePath), file, next); + }, function(err) { + if (err) { + winston.error('[themes] ' + err.stack); + } else { + winston.info('[themes] Successfully compiled templates.'); + emitter.emit('templates:compiled'); + if (callback) callback(); + } + }); + }); + }); +}; + +module.exports = Templates; \ No newline at end of file diff --git a/src/middleware/admin.js b/src/middleware/admin.js index 92ab6de9e2..0f7af52358 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -16,7 +16,9 @@ var app, middleware.isAdmin = function(req, res, next) { if (!req.user) { - return res.redirect(nconf.get('relative_path') + '/login?next=admin'); + return res.json(404, { + error: 'not-found' + }); } user.isAdministrator((req.user && req.user.uid) ? req.user.uid : 0, function (err, isAdmin) { @@ -59,7 +61,7 @@ middleware.buildHeader = function(req, res, next) { } }, function(err, pluginData) { var data = { - csrf: res.locals.csrf_token, + csrf: req.csrfToken ? req.csrfToken() : undefined, relative_path: nconf.get('relative_path'), plugins: pluginData.custom_header.plugins, authentication: pluginData.custom_header.authentication, diff --git a/src/middleware/index.js b/src/middleware/index.js index a90ea6d9a1..f9ee6e4c01 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -20,14 +20,11 @@ var utils = require('./../../public/src/utils'), compression = require('compression'), favicon = require('serve-favicon'), multipart = require('connect-multiparty'), - csrf = require('csurf'), session = require('express-session'), + cluster = require('cluster'), relativePath, - viewsPath, - themesPath, - baseTemplatesPath, - themeTemplatesPath; + themesPath; var middleware = {}; @@ -67,92 +64,26 @@ function routeCurrentTheme(app, themeId, themesData) { } // Theme's templates path - nconf.set('theme_templates_path', themeObj.templates ? path.join(themesPath, themeObj.id, themeObj.templates) : nconf.get('base_templates_path')); - themeTemplatesPath = nconf.get('theme_templates_path'); -} - -function compileTemplates(pluginTemplates) { - var mkdirp = require('mkdirp'), - rimraf = require('rimraf'); - - winston.info('[themes] Compiling templates'); - rimraf.sync(viewsPath); - mkdirp.sync(viewsPath); - - async.parallel({ - baseTpls: function(next) { - utils.walk(baseTemplatesPath, next); - }, - themeTpls: function(next) { - utils.walk(themeTemplatesPath, next); - } - }, function(err, data) { - var baseTpls = data.baseTpls, - themeTpls = data.themeTpls, - paths = {}; - - if (!baseTpls || !themeTpls) { - winston.warn('[themes] Could not find base template files at: ' + baseTemplatesPath); - } - - baseTpls = !baseTpls ? [] : baseTpls.map(function(tpl) { return tpl.replace(baseTemplatesPath, ''); }); - themeTpls = !themeTpls ? [] : themeTpls.map(function(tpl) { return tpl.replace(themeTemplatesPath, ''); }); - - baseTpls.forEach(function(el, i) { - paths[baseTpls[i]] = path.join(baseTemplatesPath, baseTpls[i]); - }); - - themeTpls.forEach(function(el, i) { - paths[themeTpls[i]] = path.join(themeTemplatesPath, themeTpls[i]); - }); - - for (var tpl in pluginTemplates) { - if (pluginTemplates.hasOwnProperty(tpl)) { - paths[tpl] = pluginTemplates[tpl]; - } - } + var themePath = nconf.get('base_templates_path'), + fallback = path.join(themesPath, themeObj.id, 'templates'); + if (themeObj.templates) { + themePath = path.join(themesPath, themeObj.id, themeObj.templates); + } else if (fs.existsSync(fallback)) { + themePath = fallback; + } - async.each(Object.keys(paths), function(relativePath, next) { - var file = fs.readFileSync(paths[relativePath]).toString(), - matches = null, - regex = /[ \t]*[ \t]*/; - - while(matches = file.match(regex)) { - var partial = "/" + matches[1]; - - if (paths[partial] && relativePath !== partial) { - file = file.replace(regex, fs.readFileSync(paths[partial]).toString()); - } else { - winston.warn('[themes] Partial not loaded: ' + matches[1]); - file = file.replace(regex, ""); - } - } - - mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/'))); - fs.writeFile(path.join(viewsPath, relativePath), file, next); - }, function(err) { - if (err) { - winston.error(err); - } else { - winston.info('[themes] Successfully compiled templates.'); - emitter.emit('templates:compiled'); - } - }); - }); + nconf.set('theme_templates_path', themePath); } module.exports = function(app, data) { middleware = require('./middleware')(app); relativePath = nconf.get('relative_path'); - viewsPath = nconf.get('views_dir'); themesPath = nconf.get('themes_path'); - baseTemplatesPath = nconf.get('base_templates_path'); - app.engine('tpl', templates.__express); app.set('view engine', 'tpl'); - app.set('views', viewsPath); + app.set('views', nconf.get('views_dir')); app.set('json spaces', process.env.NODE_ENV === 'development' ? 4 : 0); app.use(flash()); @@ -184,10 +115,8 @@ module.exports = function(app, data) { })); app.use(multipart()); - app.use(csrf()); app.use(function (req, res, next) { - res.locals.csrf_token = req.csrfToken(); res.setHeader('X-Powered-By', 'NodeBB'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); @@ -200,15 +129,11 @@ module.exports = function(app, data) { app.use(middleware.processRender); - auth.initialize(app); + auth.initialize(app, middleware); routeCurrentTheme(app, data.currentThemeId, data.themesData); routeThemeScreenshots(app, data.themesData); - - plugins.getTemplates(function(err, pluginTemplates) { - compileTemplates(pluginTemplates); - }); - + meta.templates.compile(); return middleware; }; diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 296a445ca4..508abcab39 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -15,6 +15,8 @@ var app, categories = require('./../categories'), topics = require('./../topics'), messaging = require('../messaging'), + ensureLoggedIn = require('connect-ensure-login'), + csrf = require('csurf'), controllers = { api: require('./../controllers/api') @@ -32,6 +34,10 @@ middleware.authenticate = function(req, res, next) { } }; +middleware.applyCSRF = csrf(); + +middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(); + middleware.updateLastOnlineTime = function(req, res, next) { if(req.user) { user.updateLastOnlineTime(req.user.uid); @@ -42,15 +48,54 @@ middleware.updateLastOnlineTime = function(req, res, next) { next(); }; +middleware.incrementPageViews = function(req, res, next) { + var nextMonth = new Date(), + nextDay = new Date(); + + nextMonth.setMonth(nextMonth.getMonth() + 1, 1); + nextMonth.setHours(0, 0, 0, 0); + + nextDay.setDate(nextDay.getDate() + 1); + nextDay.setHours(0, 0, 0, 0); + + db.increment('pageviews:monthly', function(err) { + if (err) { + return; + } + db.pexpireAt('pageviews:monthly', nextMonth.getTime()); + }); + + db.increment('pageviews:daily', function(err) { + if (err) { + return; + } + db.pexpireAt('pageviews:daily', nextDay.getTime()); + }); + + next(); +}; + middleware.redirectToAccountIfLoggedIn = function(req, res, next) { - if (req.user) { - user.getUserField(req.user.uid, 'userslug', function (err, userslug) { - if (res.locals.isAPI) { - return res.json(302, '/user/' + userslug); - } else { - res.redirect('/user/' + userslug); - } - }); + if (!req.user) { + return next(); + } + user.getUserField(req.user.uid, 'userslug', function (err, userslug) { + if (err) { + return next(err); + } + + if (res.locals.isAPI) { + res.json(302, '/user/' + userslug); + } else { + res.redirect('/user/' + userslug); + } + }); +}; + +middleware.redirectToLoginIfGuest = function(req, res, next) { + if (!req.user || parseInt(req.user.uid, 10) === 0) { + req.session.returnTo = req.url; + return res.redirect('/login'); } else { next(); } @@ -80,25 +125,6 @@ middleware.addSlug = function(req, res, next) { next(); }; -middleware.checkPostIndex = function(req, res, next) { - topics.getPostCount(req.params.topic_id, function(err, postCount) { - if (err) { - return next(err); - } - var postIndex = parseInt(req.params.post_index, 10); - postCount = parseInt(postCount, 10) + 1; - var url = ''; - if (postIndex > postCount) { - url = '/topic/' + req.params.topic_id + '/' + req.params.slug + '/' + postCount; - return res.locals.isAPI ? res.json(302, url) : res.redirect(url); - } else if (postIndex < 1) { - url = '/topic/' + req.params.topic_id + '/' + req.params.slug; - return res.locals.isAPI ? res.json(302, url) : res.redirect(url); - } - next(); - }); -}; - middleware.checkTopicIndex = function(req, res, next) { db.sortedSetCard('categories:' + req.params.category_id + ':tid', function(err, topicCount) { if (err) { @@ -139,7 +165,8 @@ middleware.checkGlobalPrivacySettings = function(req, res, next) { if (res.locals.isAPI) { return res.json(403, 'not-allowed'); } else { - return res.redirect('login?next=' + req.url); + req.session.returnTo = req.url; + return res.redirect('login'); } } @@ -151,7 +178,8 @@ middleware.checkAccountPermissions = function(req, res, next) { var callerUID = req.user ? parseInt(req.user.uid, 10) : 0; if (callerUID === 0) { - return res.redirect('/login?next=' + req.url); + req.session.returnTo = req.url; + return res.redirect('/login'); } user.getUidByUserslug(req.params.userslug, function (err, uid) { @@ -246,7 +274,7 @@ middleware.renderHeader = function(req, res, callback) { 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', 'brand:logo': meta.config['brand:logo'] || '', 'brand:logo:display': meta.config['brand:logo']?'':'hide', - csrf: res.locals.csrf_token, + csrf: req.csrfToken ? req.csrfToken() : undefined, navigation: custom_header.navigation, allowRegistration: meta.config.allowRegistration === undefined || parseInt(meta.config.allowRegistration, 10) === 1, searchEnabled: plugins.hasListeners('filter:search.query') @@ -391,7 +419,7 @@ middleware.processRender = function(req, res, next) { middleware.renderHeader(req, res, function(err, template) { str = template + str; - translator.translate(str, res.locals.config.defaultLang, function(translated) { + translator.translate(str, res.locals.config.userLang, function(translated) { fn(err, translated); }); }); @@ -426,6 +454,30 @@ middleware.addExpiresHeaders = function(req, res, next) { next(); }; +middleware.maintenanceMode = function(req, res, next) { + var render = function() { + res.render('maintenance', { + site_title: meta.config.site_title || 'NodeBB' + }); + } + + if (meta.config.maintenanceMode === '1') { + if (!req.user) { + return render(); + } else { + user.isAdministrator(req.user.uid, function(err, isAdmin) { + if (!isAdmin) { + return render(); + } else { + return next(); + } + }); + } + } else { + return next(); + } +}; + module.exports = function(webserver) { app = webserver; middleware.admin = require('./admin')(webserver); diff --git a/src/notifications.js b/src/notifications.js index b5d61895e5..3a10a5da37 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -5,6 +5,7 @@ var async = require('async'), cron = require('cron').CronJob, nconf = require('nconf'), S = require('string'), + _ = require('underscore'), db = require('./database'), utils = require('../public/src/utils'), @@ -20,309 +21,257 @@ var async = require('async'), if (process.env.NODE_ENV === 'development') { winston.info('[notifications.init] Registering jobs.'); } - new cron('0 0 * * *', Notifications.prune, null, true); + new cron('*/30 * * * *', Notifications.prune, null, true); }; Notifications.get = function(nid, callback) { - db.getObject('notifications:' + nid, function(err, notification) { - if (err) { - return callback(err); - } - - if (!notification) { - winston.info('[notifications.get] Could not retrieve nid ' + nid); - return callback(null, null); - } - - // Backwards compatibility for old notification schema - // Remove this block when NodeBB v0.6.0 is released. - if (notification.hasOwnProperty('text')) { - notification.bodyShort = notification.text; - notification.bodyLong = ''; - notification.text = S(notification.text).escapeHTML().s; - } - - notification.bodyShort = S(notification.bodyShort).escapeHTML().s; - notification.bodyLong = S(notification.bodyLong).escapeHTML().s; - - if (notification.from && !notification.image) { - User.getUserField(notification.from, 'picture', function(err, picture) { - if (err) { - return callback(err); - } - notification.image = picture; - callback(null, notification); - }); - return; - } else if (notification.image) { - switch(notification.image) { - case 'brand:logo': - notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; - break; - } - - return callback(null, notification); - } - - callback(null, notification); + Notifications.getMultiple([nid], function(err, notifications) { + callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null); }); }; - Notifications.create = function(data, callback) { - // Add default values to data Object if not already set - var defaults = { - bodyShort: '', - bodyLong: '', - path: '', - importance: 5, - datetime: Date.now(), - uniqueId: utils.generateUUID() - }; - - for(var v in defaults) { - if (defaults.hasOwnProperty(v) && !data[v]) { - data[v] = defaults[v]; - } - } - - // Backwards compatibility for old notification schema - // Remove this block for NodeBB v0.6.0 - if (data.hasOwnProperty('text')) { - data.bodyShort = data.text; - data.bodyLong = ''; - delete data.text; - } + Notifications.getMultiple = function(nids, callback) { + var keys = nids.map(function(nid) { + return 'notifications:' + nid; + }); - db.incrObjectField('global', 'nextNid', function(err, nid) { + db.getObjects(keys, function(err, notifications) { if (err) { return callback(err); } - data.nid = nid; - db.setAdd('notifications', nid); - db.setObject('notifications:' + nid, data, function(err) { - callback(err, nid); - }); - }); - }; - - Notifications.push = function(nid, uids, callback) { - callback = callback || function() {}; - var websockets = require('./socket.io'); - if (!Array.isArray(uids)) { - uids = [uids]; - } - - Notifications.get(nid, function(err, notif_data) { - if (err) { - return callback(err); + if (!Array.isArray(notifications) || !notifications.length) { + return callback(null, []); } - async.each(uids, function(uid, next) { - if (!parseInt(uid, 10)) { - return next(); + async.map(notifications, function(notification, next) { + if (!notification) { + return next(null, null); } - shouldPush(uid, notif_data, function(err, shouldPush) { - if (err || !shouldPush) { - return callback(err); - } + // Backwards compatibility for old notification schema + // Remove this block when NodeBB v0.6.0 is released. + if (notification.hasOwnProperty('text')) { + notification.bodyShort = notification.text; + notification.bodyLong = ''; + notification.text = S(notification.text).escapeHTML().s; + } + + if (notification.bodyShort) { + notification.bodyShort = S(notification.bodyShort).escapeHTML().s; + } + if (notification.bodyLong) { + notification.bodyLong = S(notification.bodyLong).escapeHTML().s; + } - async.parallel([ - async.apply(db.setObjectField, 'uid:' + uid + ':notifications:uniqueId:nid', notif_data.uniqueId, nid), - async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notif_data.datetime, notif_data.uniqueId), - async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', notif_data.uniqueId) - ], function(err) { + if (notification.from && !notification.image) { + User.getUserField(notification.from, 'picture', function(err, picture) { if (err) { return next(err); } + notification.image = picture; + next(null, notification); + }); + return; + } else if (notification.image) { + switch(notification.image) { + case 'brand:logo': + notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; + break; + } - User.notifications.getUnreadCount(uid, function(err, count) { - if (!err) { - websockets.in('uid_' + uid).emit('event:new_notification', notif_data, count); - } - }); + return next(null, notification); + } - // Plugins - notif_data.uid = uid; - plugins.fireHook('action:notification.pushed', notif_data); - next(); - }); - }); }, callback); }); }; - function shouldPush(uid, newNotifObj, callback) { - if (!newNotifObj) { - return callback(null, false); + Notifications.create = function(data, callback) { + if (!data.nid) { + return callback(new Error('no-notification-id')); } - - hasNotification(newNotifObj.uniqueId, uid, function(err, hasNotification) { + data.importance = data.importance || 5; + db.getObject('notifications:' + data.nid, function(err, oldNotification) { if (err) { return callback(err); } - if (!hasNotification) { - return callback(null, true); + if (oldNotification) { + if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) { + return callback(); + } } - db.getObjectField('uid:' + uid + ':notifications:uniqueId:nid', newNotifObj.uniqueId, function(err, nid) { - if (err) { - return callback(err); + var now = Date.now(); + data.datetime = now; + async.parallel([ + function(next) { + db.sortedSetAdd('notifications', now, data.nid, next); + }, + function(next) { + db.setObject('notifications:' + data.nid, data, next); } + ], function(err) { + callback(err, data); + }); + }); + }; - db.getObjectFields('notifications:' + nid, ['nid', 'uniqueId', 'importance'], function(err, oldNotifObj) { - if (err) { - return callback(err); - } + Notifications.push = function(notification, uids, callback) { + callback = callback || function() {}; - if (!oldNotifObj || newNotifObj.uniqueId !== oldNotifObj.uniqueId) { - return callback(null, true); - } + if (!notification.nid) { + return callback(); + } - callback(null, parseInt(newNotifObj.importance, 10) >= parseInt(oldNotifObj.importance, 10)); - }); - }); + var websockets = require('./socket.io'); + if (!Array.isArray(uids)) { + uids = [uids]; + } + + var unreadKeys = []; + var readKeys = []; + + uids.filter(Boolean).forEach(function(uid) { + unreadKeys.push('uid:' + uid + ':notifications:unread'); + readKeys.push('uid:' + uid + ':notifications:read'); }); - } - function hasNotification(uniqueId, uid, callback) { async.parallel([ - async.apply(db.isSortedSetMember, 'uid:' + uid + ':notifications:unread', uniqueId), - async.apply(db.isSortedSetMember, 'uid:' + uid + ':notifications:read', uniqueId) - ], function(err, results) { + function(next) { + db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next); + }, + function(next) { + db.sortedSetsRemove(readKeys, notification.nid, next); + } + ], function(err) { if (err) { return callback(err); } - callback(null, results[0] || results[1]); + var oneWeekAgo = Date.now() - 604800000; + db.sortedSetsRemoveRangeByScore(unreadKeys, 0, oneWeekAgo); + db.sortedSetsRemoveRangeByScore(readKeys, 0, oneWeekAgo); + + plugins.fireHook('action:notification.pushed', {notification: notification, uids: uids}); + callback(); + + for(var i=0; i 0) { - nids = [nids]; - } - - async.each(nids, function(nid, next) { - Notifications.markRead(nid, uid, next); - }, callback); - }; - Notifications.markAllRead = function(uid, callback) { - db.getObjectValues('uid:' + uid + ':notifications:uniqueId:nid', function(err, nids) { + db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, 99, function(err, nids) { if (err) { return callback(err); } if (!Array.isArray(nids) || !nids.length) { - return callback(err); + return callback(); } Notifications.markReadMultiple(nids, uid, callback); }); }; - Notifications.markReadByUniqueId = function(uid, uniqueId, callback) { - async.waterfall([ - async.apply(db.getObjectField, 'uid:' + uid + ':notifications:uniqueId:nid', uniqueId), - function(nid, next) { - Notifications.markRead(nid, uid, next); - } - ], callback); - }; - - Notifications.prune = function(cutoff) { + Notifications.prune = function() { var start = process.hrtime(); if (process.env.NODE_ENV === 'development') { winston.info('[notifications.prune] Removing expired notifications from the database.'); } - var today = new Date(), + var week = 604800000, numPruned = 0; - if (!cutoff) { - cutoff = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); - } - - var cutoffTime = cutoff.getTime(); + var cutoffTime = Date.now() - week; - db.getSetMembers('notifications', function(err, nids) { + db.getSortedSetRangeByScore('notifications', 0, 500, 0, cutoffTime, function(err, nids) { if (err) { return winston.error(err.message); } - var keys = nids.map(function(nid) { + if (!Array.isArray(nids) || !nids.length) { + return events.log('No notifications to prune'); + } + + var keys = nids.map(function(nid) { return 'notifications:' + nid; }); - db.getObjectsFields(keys, ['nid', 'datetime'], function(err, notifs) { + numPruned = nids.length; + + events.log('Notification pruning. Expired Nids = ' + numPruned); + + async.parallel([ + function(next) { + db.sortedSetRemove('notifications', nids, next); + }, + function(next) { + db.deleteAll(keys, next); + } + ], function(err) { if (err) { - return winston.error(err.message); + return winston.error('Encountered error pruning notifications: ' + err.message); } - var expiredNids = notifs.filter(function(notif) { - return notif && parseInt(notif.datetime, 10) < cutoffTime; - }).map(function(notif) { - return notif.nid; - }); - - async.eachLimit(expiredNids, 50, function(nid, next) { - async.parallel([ - function(next) { - db.setRemove('notifications', nid, next); - }, - function(next) { - db.delete('notifications:' + nid, next); - } - ], function(err) { - numPruned++; - next(err); - }); - }, function(err) { - if (err) { - return winston.error('Encountered error pruning notifications: ' + err.message); - } - - if (process.env.NODE_ENV === 'development') { - winston.info('[notifications.prune] Notification pruning completed. ' + numPruned + ' expired notification' + (numPruned !== 1 ? 's' : '') + ' removed.'); - } - var diff = process.hrtime(start); - events.log('Pruning '+ numPruned + ' notifications took : ' + (diff[0] * 1e3 + diff[1] / 1e6) + ' ms'); - }); + if (process.env.NODE_ENV === 'development') { + winston.info('[notifications.prune] Notification pruning completed. ' + numPruned + ' expired notification' + (numPruned !== 1 ? 's' : '') + ' removed.'); + } + var diff = process.hrtime(start); + events.log('Pruning '+ numPruned + ' notifications took : ' + (diff[0] * 1e3 + diff[1] / 1e6) + ' ms'); }); }); }; diff --git a/src/password.js b/src/password.js new file mode 100644 index 0000000000..c4a7febca8 --- /dev/null +++ b/src/password.js @@ -0,0 +1,51 @@ + + +'use strict'; +var fork = require('child_process').fork; + +(function(module) { + + var child = fork('./bcrypt', process.argv.slice(2), { + env: process.env + }); + + var callbacks = { + 'hash': {}, + 'compare': {} + }; + + module.hash = function(rounds, password, callback) { + sendCommand({type: 'hash', password: password, rounds: rounds}, callback); + }; + + module.compare = function(password, hash, callback) { + sendCommand({type: 'compare', password: password, hash: hash}, callback); + }; + + function sendCommand(data, callback) { + callbacks[data.type][data.password] = callbacks[data.type][data.password] || []; + callbacks[data.type][data.password].push(callback); + child.send(data); + } + + child.on('message', function(msg) { + var cbs = callbacks[msg.type] ? callbacks[msg.type][msg.password] : null; + + if (Array.isArray(cbs)) { + if (msg.err) { + var err = new Error(msg.err.message); + cbs.forEach(function(callback) { + callback(err); + }); + cbs.length = 0; + return; + } + + cbs.forEach(function(callback) { + callback(null, msg.type === 'hash' ? msg.hash : msg.result); + }); + cbs.length = 0; + } + }); + +}(exports)); diff --git a/src/plugins.js b/src/plugins.js index d554edd6d3..2cc51316d1 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -6,13 +6,18 @@ var fs = require('fs'), winston = require('winston'), nconf = require('nconf'), semver = require('semver'), + express = require('express'), db = require('./database'), emitter = require('./emitter'), meta = require('./meta'), translator = require('../public/src/translator'), utils = require('../public/src/utils'), - pkg = require('../package.json'); + hotswap = require('./hotswap'), + pkg = require('../package.json'), + + controllers = require('./controllers'), + app, middleware; (function(Plugins) { @@ -26,11 +31,15 @@ var fs = require('fs'), Plugins.initialized = false; - Plugins.init = function() { + Plugins.init = function(nbbApp, nbbMiddleware) { if (Plugins.initialized) { return; } + app = nbbApp; + middleware = nbbMiddleware; + hotswap.prepare(nbbApp); + if (global.env === 'development') { winston.info('[plugins] Initializing plugins system'); } @@ -107,18 +116,44 @@ var fs = require('fs'), }); next(); - } + }, + async.apply(Plugins.reloadRoutes) ], callback); }; + Plugins.reloadRoutes = function(callback) { + var router = express.Router(); + router.hotswapId = 'plugins'; + router.render = function() { + app.render.apply(app, arguments); + }; + + // Deprecated as of v0.5.0, remove this hook call for NodeBB v0.6.0-1 + Plugins.fireHook('action:app.load', router, middleware, controllers); + + Plugins.fireHook('static:app.load', router, middleware, controllers, function() { + hotswap.replace('plugins', router); + winston.info('[plugins] All plugins reloaded and rerouted'); + callback(); + }); + }; + Plugins.loadPlugin = function(pluginPath, callback) { fs.readFile(path.join(pluginPath, 'plugin.json'), function(err, data) { if (err) { return callback(pluginPath.match('nodebb-theme') ? null : err); } - var pluginData = JSON.parse(data), - libraryPath, staticDir; + try { + var pluginData = JSON.parse(data), + libraryPath, staticDir; + } catch (err) { + var pluginDir = pluginPath.split(path.sep); + pluginDir = pluginDir[pluginDir.length -1]; + + winston.error('[plugins/' + pluginDir + '] Plugin not loaded - please check its plugin.json for errors'); + return callback(); + } /* Starting v0.5.0, `minver` is deprecated in favour of `compatibility`. @@ -497,7 +532,6 @@ var fs = require('fs'), // Reload meta data Plugins.reload(function() { - if(!active) { Plugins.fireHook('action:plugin.activate', id); } diff --git a/src/postTools.js b/src/postTools.js index 97d3e71928..cfbd9d504f 100644 --- a/src/postTools.js +++ b/src/postTools.js @@ -54,33 +54,44 @@ var winston = require('winston'), if (err) { return next(err); } + options.tags = options.tags || []; - if (isMainPost) { - title = title.trim(); - - var topicData = { - title: title, - slug: tid + '/' + utils.slugify(title) - }; - if (options.topic_thumb) { - topicData.thumb = options.topic_thumb; - } - db.setObject('topic:' + tid, topicData, function(err) { - plugins.fireHook('action:topic.edit', tid); + if (!isMainPost) { + return next(null, { + tid: tid, + isMainPost: false }); + } - topics.updateTags(tid, options.tags); + title = title.trim(); + + var topicData = { + title: title, + slug: tid + '/' + utils.slugify(title) + }; + if (options.topic_thumb) { + topicData.thumb = options.topic_thumb; } - next(null, { - tid: tid, - title: validator.escape(title), - isMainPost: isMainPost, - tags: options.tags.map(function(tag) { return {name:tag}; }) + db.setObject('topic:' + tid, topicData, function(err) { + plugins.fireHook('action:topic.edit', tid); }); - }); + topics.updateTags(tid, options.tags, function(err) { + if (err) { + return next(err); + } + topics.getTopicTagsObjects(tid, function(err, tags) { + next(err, { + tid: tid, + title: validator.escape(title), + isMainPost: isMainPost, + tags: tags + }); + }); + }); + }); }, content: function(next) { PostTools.parse(postData.content, next); @@ -90,7 +101,7 @@ var winston = require('winston'), return callback(err); } - events.logPostEdit(uid, pid); + //events.logPostEdit(uid, pid); plugins.fireHook('action:post.edit', postData); callback(null, results); }); @@ -200,7 +211,7 @@ var winston = require('winston'), } if (timestamp) { - topics.updateTimestamp(tid, timestamp); + return topics.updateTimestamp(tid, timestamp, callback); } callback(); }); diff --git a/src/posts.js b/src/posts.js index 856145c4c0..dcedf4383d 100644 --- a/src/posts.js +++ b/src/posts.js @@ -11,7 +11,7 @@ var async = require('async'), db = require('./database'), - utils = require('./../public/src/utils'), + utils = require('../public/src/utils'), user = require('./user'), groups = require('./groups'), topics = require('./topics'), @@ -21,9 +21,11 @@ var async = require('async'), categories = require('./categories'), plugins = require('./plugins'), meta = require('./meta'), - emitter = require('./emitter'); + emitter = require('./emitter'), + websockets = require('./socket.io'); (function(Posts) { + require('./posts/recent')(Posts); require('./posts/delete')(Posts); Posts.create = function(data, callback) { @@ -67,18 +69,33 @@ var async = require('async'), function(postData, next) { db.setObject('post:' + postData.pid, postData, next); }, - function(result, next) { - db.sortedSetAdd('posts:pid', timestamp, postData.pid); - - db.incrObjectField('global', 'postCount'); - - emitter.emit('event:newpost', postData); - - plugins.fireHook('filter:post.get', postData, next); + function(next) { + async.parallel([ + function(next) { + user.onNewPostMade(postData, next); + }, + function(next) { + topics.onNewPostMade(postData, next); + }, + function(next) { + categories.onNewPostMade(postData, next); + }, + function(next) { + db.sortedSetAdd('posts:pid', timestamp, postData.pid, next); + }, + function(next) { + db.incrObjectField('global', 'postCount', next); + } + ], function(err) { + if (err) { + return next(err); + } + plugins.fireHook('filter:post.get', postData, next); + }); }, function(postData, next) { postTools.parse(postData.content, function(err, content) { - if(err) { + if (err) { return next(err); } @@ -92,7 +109,7 @@ var async = require('async'), ], callback); }; - Posts.getPostsByTid = function(tid, set, start, end, reverse, callback) { + Posts.getPostsByTid = function(tid, set, start, end, uid, reverse, callback) { Posts.getPidsFromSet(set, start, end, reverse, function(err, pids) { if(err) { return callback(err); @@ -102,15 +119,18 @@ var async = require('async'), return callback(null, []); } - Posts.getPostsByPids(pids, tid, callback); + Posts.getPostsByPids(pids, uid, callback); }); }; Posts.getPidsFromSet = function(set, start, end, reverse, callback) { + if (isNaN(start) || isNaN(end)) { + return callback(null, []); + } db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, end, callback); }; - Posts.getPostsByPids = function(pids, tid, callback) { + Posts.getPostsByPids = function(pids, uid, callback) { var keys = []; for(var x=0, numPids=pids.length; x 0) { @@ -33,11 +36,17 @@ var continueLogin = function() { passport.authenticate('local', function(err, userData, info) { if (err) { - return res.json(403, err.message); + req.flash('error', info); + return res.redirect(nconf.get('relative_path') + '/login'); } if (!userData) { - return res.json(403, info); + if (typeof info === 'object') { + info = '[[error:invalid-username-or-password]]'; + } + + req.flash('error', info); + return res.redirect(nconf.get('relative_path') + '/login'); } // Alter user cookie depending on passed-in option @@ -55,9 +64,17 @@ }, function() { if (userData.uid) { user.logIP(userData.uid, req.ip); + + plugins.fireHook('action:user.loggedIn', userData.uid); } - res.json(200, info); + if (!req.session.returnTo) { + res.redirect(nconf.get('relative_path') + '/'); + } else { + var next = req.session.returnTo; + delete req.session.returnTo; + res.redirect(nconf.get('relative_path') + next); + } }); })(req, res, next); }; @@ -121,22 +138,26 @@ }); } - Auth.initialize = function(app) { + Auth.initialize = function(app, middleware) { app.use(passport.initialize()); app.use(passport.session()); - }; + Auth.app = app; + Auth.middleware = middleware; + }; Auth.get_login_strategies = function() { return login_strategies; }; - Auth.registerApp = function(app) { - Auth.app = app; - }; + Auth.reloadRoutes = function(callback) { + var router = express.Router(); + router.hotswapId = 'auth'; - Auth.createRoutes = function(app, middleware, controllers) { plugins.ready(function() { + // Reset the registered login strategies + login_strategies.length = 0; + plugins.fireHook('filter:auth.init', login_strategies, function(err) { if (err) { winston.error('filter:auth.init - plugin failure'); @@ -159,13 +180,13 @@ /* End backwards compatibility block */ if (strategy.url) { - app.get(strategy.url, passport.authenticate(strategy.name, { + router.get(strategy.url, passport.authenticate(strategy.name, { scope: strategy.scope })); } - app.get(strategy.callbackURL, passport.authenticate(strategy.name, { - successRedirect: nconf.get('relative_path') + '/', + router.get(strategy.callbackURL, passport.authenticate(strategy.name, { + successReturnToOrRedirect: nconf.get('relative_path') + '/', failureRedirect: nconf.get('relative_path') + '/login' })); } @@ -184,16 +205,22 @@ } /* End backwards compatibility block */ - app.post('/logout', logout); - app.post('/register', register); - app.post('/login', login); + router.post('/logout', logout); + router.post('/register', Auth.middleware.applyCSRF, register); + router.post('/login', Auth.middleware.applyCSRF, login); + + hotswap.replace('auth', router); + if (typeof callback === 'function') { + callback(); + } }); }); }; Auth.login = function(username, password, next) { if (!username || !password) { - return next(new Error('[[error:invalid-user-data]]')); + next(new Error('[[error:invalid-password]]')); + return; } var userslug = utils.slugify(username); @@ -204,7 +231,6 @@ } if(!uid) { - // To-do: Even if a user doesn't exist, compare passwords anyway, so we don't immediately return return next(null, false, '[[error:no-user]]'); } @@ -226,7 +252,7 @@ return next(null, false, '[[error:user-banned]]'); } - bcrypt.compare(password, userData.password, function(err, res) { + Password.compare(password, userData.password, function(err, res) { if (err) { return next(new Error('bcrypt compare error')); } diff --git a/src/routes/debug.js b/src/routes/debug.js index a03c5b2649..584a70a6b6 100644 --- a/src/routes/debug.js +++ b/src/routes/debug.js @@ -56,6 +56,6 @@ module.exports = function(app, middleware, controllers) { }); router.get('/test', function(req, res) { - res.redirect('404'); + res.redirect(404); }); }; diff --git a/src/routes/index.js b/src/routes/index.js index e843175098..ed76d01fe3 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -18,191 +18,147 @@ var nconf = require('nconf'), function mainRoutes(app, middleware, controllers) { - app.get('/', middleware.buildHeader, controllers.home); - app.get('/api', controllers.home); + setupPageRoute(app, '/', middleware, [], controllers.home); - app.get('/login', middleware.redirectToAccountIfLoggedIn, middleware.buildHeader, controllers.login); - app.get('/api/login', middleware.redirectToAccountIfLoggedIn, controllers.login); + var loginRegisterMiddleware = [middleware.applyCSRF, middleware.redirectToAccountIfLoggedIn]; - app.get('/register', middleware.redirectToAccountIfLoggedIn, middleware.buildHeader, controllers.register); - app.get('/api/register', middleware.redirectToAccountIfLoggedIn, controllers.register); - - app.get('/confirm/:code', middleware.buildHeader, controllers.confirmEmail); - app.get('/api/confirm/:code', controllers.confirmEmail); - - app.get('/outgoing', middleware.buildHeader, controllers.outgoing); - app.get('/api/outgoing', controllers.outgoing); - - app.get('/search/:term?', middleware.buildHeader, middleware.guestSearchingAllowed, controllers.search); - app.get('/api/search/:term?', middleware.guestSearchingAllowed, controllers.search); - - app.get('/reset/:code?', middleware.buildHeader, controllers.reset); - app.get('/api/reset/:code?', controllers.reset); + setupPageRoute(app, '/login', middleware, loginRegisterMiddleware, controllers.login); + setupPageRoute(app, '/register', middleware, loginRegisterMiddleware, controllers.register); + setupPageRoute(app, '/confirm/:code', middleware, [], controllers.confirmEmail); + setupPageRoute(app, '/outgoing', middleware, [], controllers.outgoing); + setupPageRoute(app, '/search/:term?', middleware, [middleware.guestSearchingAllowed], controllers.search); + setupPageRoute(app, '/reset/:code?', middleware, [], controllers.reset); } function staticRoutes(app, middleware, controllers) { - app.get('/404', middleware.buildHeader, controllers.static['404']); - app.get('/api/404', controllers.static['404']); - - app.get('/403', middleware.buildHeader, controllers.static['403']); - app.get('/api/403', controllers.static['403']); - - app.get('/500', middleware.buildHeader, controllers.static['500']); - app.get('/api/500', controllers.static['500']); + setupPageRoute(app, '/404', middleware, [], controllers.static['404']); + setupPageRoute(app, '/403', middleware, [], controllers.static['403']); + setupPageRoute(app, '/500', middleware, [], controllers.static['500']); } function topicRoutes(app, middleware, controllers) { app.get('/api/topic/teaser/:topic_id', controllers.topics.teaser); - app.get('/topic/:topic_id/:slug/:post_index?', middleware.buildHeader, middleware.checkPostIndex, controllers.topics.get); - app.get('/api/topic/:topic_id/:slug/:post_index?', middleware.checkPostIndex, controllers.topics.get); - - app.get('/topic/:topic_id/:slug?', middleware.buildHeader, middleware.addSlug, controllers.topics.get); - app.get('/api/topic/:topic_id/:slug?', middleware.addSlug, controllers.topics.get); + setupPageRoute(app, '/topic/:topic_id/:slug/:post_index?', middleware, [middleware.applyCSRF], controllers.topics.get); + setupPageRoute(app, '/topic/:topic_id/:slug?', middleware, [middleware.applyCSRF, middleware.addSlug], controllers.topics.get); } function tagRoutes(app, middleware, controllers) { - - app.get('/tags/:tag', middleware.buildHeader, controllers.tags.getTag); - app.get('/api/tags/:tag', controllers.tags.getTag); - - app.get('/tags', middleware.buildHeader, controllers.tags.getTags); - app.get('/api/tags', controllers.tags.getTags); + setupPageRoute(app, '/tags/:tag', middleware, [], controllers.tags.getTag); + setupPageRoute(app, '/tags', middleware, [], controllers.tags.getTags); } function categoryRoutes(app, middleware, controllers) { - app.get('/popular/:term?', middleware.buildHeader, controllers.categories.popular); - app.get('/api/popular/:term?', controllers.categories.popular); - - app.get('/recent/:term?', middleware.buildHeader, controllers.categories.recent); - app.get('/api/recent/:term?', controllers.categories.recent); - - app.get('/unread', middleware.buildHeader, middleware.authenticate, controllers.categories.unread); - app.get('/api/unread', middleware.authenticate, controllers.categories.unread); - + setupPageRoute(app, '/popular/:term?', middleware, [], controllers.categories.popular); + setupPageRoute(app, '/recent/:term?', middleware, [], controllers.categories.recent); + setupPageRoute(app, '/unread', middleware, [middleware.authenticate], controllers.categories.unread); app.get('/api/unread/total', middleware.authenticate, controllers.categories.unreadTotal); - app.get('/category/:category_id/:slug/:topic_index', middleware.buildHeader, middleware.checkTopicIndex, controllers.categories.get); - app.get('/api/category/:category_id/:slug/:topic_index', middleware.checkTopicIndex, controllers.categories.get); - - app.get('/category/:category_id/:slug?', middleware.buildHeader, middleware.addSlug, controllers.categories.get); - app.get('/api/category/:category_id/:slug?', controllers.categories.get); + setupPageRoute(app, '/category/:category_id/:slug/:topic_index', middleware, [middleware.applyCSRF, middleware.checkTopicIndex], controllers.categories.get); + setupPageRoute(app, '/category/:category_id/:slug?', middleware, [middleware.applyCSRF, middleware.addSlug], controllers.categories.get); } function accountRoutes(app, middleware, controllers) { + var middlewares = [middleware.checkGlobalPrivacySettings]; + var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions]; - app.get('/user/:userslug', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.accounts.getAccount); - app.get('/api/user/:userslug', middleware.checkGlobalPrivacySettings, controllers.accounts.getAccount); - - app.get('/user/:userslug/following', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.accounts.getFollowing); - app.get('/api/user/:userslug/following', middleware.checkGlobalPrivacySettings, controllers.accounts.getFollowing); + setupPageRoute(app, '/user/:userslug', middleware, middlewares, controllers.accounts.getAccount); + setupPageRoute(app, '/user/:userslug/following', middleware, middlewares, controllers.accounts.getFollowing); + setupPageRoute(app, '/user/:userslug/followers', middleware, middlewares, controllers.accounts.getFollowers); + setupPageRoute(app, '/user/:userslug/posts', middleware, middlewares, controllers.accounts.getPosts); + setupPageRoute(app, '/user/:userslug/topics', middleware, middlewares, controllers.accounts.getTopics); - app.get('/user/:userslug/followers', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.accounts.getFollowers); - app.get('/api/user/:userslug/followers', middleware.checkGlobalPrivacySettings, controllers.accounts.getFollowers); + setupPageRoute(app, '/user/:userslug/favourites', middleware, accountMiddlewares, controllers.accounts.getFavourites); + setupPageRoute(app, '/user/:userslug/edit', middleware, accountMiddlewares, controllers.accounts.accountEdit); + setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.accountSettings); - app.get('/user/:userslug/favourites', middleware.buildHeader, middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, controllers.accounts.getFavourites); - app.get('/api/user/:userslug/favourites', middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, controllers.accounts.getFavourites); - - app.get('/user/:userslug/posts', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.accounts.getPosts); - app.get('/api/user/:userslug/posts', middleware.checkGlobalPrivacySettings, controllers.accounts.getPosts); - - app.get('/user/:userslug/topics', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.accounts.getTopics); - app.get('/api/user/:userslug/topics', middleware.checkGlobalPrivacySettings, controllers.accounts.getTopics); - - app.get('/user/:userslug/edit', middleware.buildHeader, middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, controllers.accounts.accountEdit); - app.get('/api/user/:userslug/edit', middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, controllers.accounts.accountEdit); - - app.get('/user/:userslug/settings', middleware.buildHeader, middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, controllers.accounts.accountSettings); - app.get('/api/user/:userslug/settings', middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, controllers.accounts.accountSettings); - - app.get('/notifications', middleware.buildHeader, middleware.authenticate, controllers.accounts.getNotifications); - app.get('/api/notifications', middleware.authenticate, controllers.accounts.getNotifications); - - app.get('/chats/:userslug?', middleware.buildHeader, middleware.authenticate, controllers.accounts.getChats); - app.get('/api/chats/:userslug?', middleware.authenticate, controllers.accounts.getChats); + setupPageRoute(app, '/notifications', middleware, [middleware.authenticate], controllers.accounts.getNotifications); + setupPageRoute(app, '/chats/:userslug?', middleware, [middleware.redirectToLoginIfGuest], controllers.accounts.getChats); } function userRoutes(app, middleware, controllers) { - app.get('/users', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.users.getOnlineUsers); - app.get('/api/users', middleware.checkGlobalPrivacySettings, controllers.users.getOnlineUsers); - - app.get('/users/online', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.users.getOnlineUsers); - app.get('/api/users/online', middleware.checkGlobalPrivacySettings, controllers.users.getOnlineUsers); + var middlewares = [middleware.checkGlobalPrivacySettings]; - app.get('/users/sort-posts', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.users.getUsersSortedByPosts); - app.get('/api/users/sort-posts', middleware.checkGlobalPrivacySettings, controllers.users.getUsersSortedByPosts); + setupPageRoute(app, '/users', middleware, middlewares, controllers.users.getOnlineUsers); + setupPageRoute(app, '/users/online', middleware, middlewares, controllers.users.getOnlineUsers); + setupPageRoute(app, '/users/sort-posts', middleware, middlewares, controllers.users.getUsersSortedByPosts); + setupPageRoute(app, '/users/sort-reputation', middleware, middlewares, controllers.users.getUsersSortedByReputation); + setupPageRoute(app, '/users/latest', middleware, middlewares, controllers.users.getUsersSortedByJoinDate); + setupPageRoute(app, '/users/search', middleware, middlewares, controllers.users.getUsersForSearch); + } - app.get('/users/sort-reputation', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.users.getUsersSortedByReputation); - app.get('/api/users/sort-reputation', middleware.checkGlobalPrivacySettings, controllers.users.getUsersSortedByReputation); - - app.get('/users/latest', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.users.getUsersSortedByJoinDate); - app.get('/api/users/latest', middleware.checkGlobalPrivacySettings, controllers.users.getUsersSortedByJoinDate); +function groupRoutes(app, middleware, controllers) { + var middlewares = [middleware.checkGlobalPrivacySettings]; - app.get('/users/search', middleware.buildHeader, middleware.checkGlobalPrivacySettings, controllers.users.getUsersForSearch); - app.get('/api/users/search', middleware.checkGlobalPrivacySettings, controllers.users.getUsersForSearch); + setupPageRoute(app, '/groups', middleware, middlewares, controllers.groups.list); + setupPageRoute(app, '/groups/:name', middleware, middlewares, controllers.groups.details); } -function groupRoutes(app, middleware, controllers) { - app.get('/groups', middleware.buildHeader, controllers.groups.list); - app.get('/api/groups', controllers.groups.list); +function setupPageRoute(router, name, middleware, middlewares, controller) { + middlewares = middlewares.concat([middleware.incrementPageViews, middleware.updateLastOnlineTime]); - app.get('/groups/:name', middleware.buildHeader, controllers.groups.details); - app.get('/api/groups/:name', controllers.groups.details); + router.get(name, middleware.buildHeader, middlewares, controller); + router.get('/api' + name, middlewares, controller); } - module.exports = function(app, middleware) { - plugins.ready(function() { - var router = express.Router(), - relativePath = nconf.get('relative_path'); - - router.render = function() { - app.render.apply(app, arguments); - }; - - app.all(relativePath + '/api/?*', middleware.updateLastOnlineTime, middleware.prepareAPI); - app.all(relativePath + '/api/admin/*', middleware.admin.isAdmin, middleware.prepareAPI); - app.all(relativePath + '/admin/*', middleware.admin.isAdmin); - app.get(relativePath + '/admin', middleware.admin.isAdmin); - - // Deprecated as of v0.5.0, remove this hook call for NodeBB v0.6.0-1 - plugins.fireHook('action:app.load', router, middleware, controllers); - - adminRoutes(router, middleware, controllers); - metaRoutes(router, middleware, controllers); - apiRoutes(router, middleware, controllers); - feedRoutes(router, middleware, controllers); - pluginRoutes(router, middleware, controllers); - authRoutes.createRoutes(router, middleware, controllers); - - /** - * Every view has an associated API route. - * - */ - mainRoutes(router, middleware, controllers); - staticRoutes(router, middleware, controllers); - topicRoutes(router, middleware, controllers); - tagRoutes(router, middleware, controllers); - categoryRoutes(router, middleware, controllers); - accountRoutes(router, middleware, controllers); - userRoutes(router, middleware, controllers); - groupRoutes(router, middleware, controllers); - - plugins.fireHook('static:app.load', router, middleware, controllers, function() { - app.use(relativePath, router); - - app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), { - maxAge: app.enabled('cache') ? 5184000000 : 0 - })); - app.use(catch404); - app.use(handleErrors); - }); - }); + var router = express.Router(), + pluginRouter = express.Router(), + authRouter = express.Router(), + relativePath = nconf.get('relative_path'); + + pluginRouter.render = function() { + app.render.apply(app, arguments); + }; + + // Set-up for hotswapping (when NodeBB reloads) + pluginRouter.hotswapId = 'plugins'; + authRouter.hotswapId = 'auth'; + + app.use(middleware.maintenanceMode); + + app.all(relativePath + '/api/?*', middleware.prepareAPI); + app.all(relativePath + '/api/admin/*', middleware.admin.isAdmin, middleware.prepareAPI); + app.all(relativePath + '/admin/?*', middleware.ensureLoggedIn, middleware.admin.isAdmin); + + adminRoutes(router, middleware, controllers); + metaRoutes(router, middleware, controllers); + apiRoutes(router, middleware, controllers); + feedRoutes(router, middleware, controllers); + pluginRoutes(router, middleware, controllers); + + /** + * Every view has an associated API route. + * + */ + + mainRoutes(router, middleware, controllers); + staticRoutes(router, middleware, controllers); + topicRoutes(router, middleware, controllers); + tagRoutes(router, middleware, controllers); + categoryRoutes(router, middleware, controllers); + accountRoutes(router, middleware, controllers); + userRoutes(router, middleware, controllers); + groupRoutes(router, middleware, controllers); + + app.use(relativePath, router); + app.use(relativePath, pluginRouter); + app.use(relativePath, authRouter); if (process.env.NODE_ENV === 'development') { require('./debug')(app, middleware, controllers); } + app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), { + maxAge: app.enabled('cache') ? 5184000000 : 0 + })); + + app.use(catch404); + app.use(handleErrors); + + // Add plugin routes + plugins.init(app, middleware); + authRoutes.reloadRoutes(); }; function handleErrors(err, req, res, next) { diff --git a/src/routes/meta.js b/src/routes/meta.js index f0d9aa85b2..76cefadf0e 100644 --- a/src/routes/meta.js +++ b/src/routes/meta.js @@ -35,8 +35,8 @@ function setupPluginSourceMapping(app) { development mode (`./nodebb dev`) */ var routes = plugins.clientScripts, - mapping, - prefix = __dirname.split(path.sep).length - 1; + prefix = __dirname.split(path.sep).length - 1, + mapping; routes.forEach(function(route) { mapping = '/' + route.split(path.sep).slice(prefix).join('/'); diff --git a/src/routes/plugins.js b/src/routes/plugins.js index fd195ca3f2..17efdb8c53 100644 --- a/src/routes/plugins.js +++ b/src/routes/plugins.js @@ -8,8 +8,7 @@ var _ = require('underscore'), async = require('async'), winston = require('winston'), - plugins = require('../plugins'), - pluginRoutes = []; + plugins = require('../plugins'); module.exports = function(app, middleware, controllers) { diff --git a/src/search.js b/src/search.js index 808958ad73..0f55c6aafb 100644 --- a/src/search.js +++ b/src/search.js @@ -50,7 +50,7 @@ search.search = function(term, uid, callback) { return callback(err); } - posts.getPostSummaryByPids(pids, {stripTags: true, parse: false}, function(err, posts) { + posts.getPostSummaryByPids(pids, uid, {stripTags: true, parse: false}, function(err, posts) { if (err) { return callback(err); } @@ -73,9 +73,9 @@ function getMainPids(tids, callback) { if (err) { return callback(err); } - topics = topics.map(function(topics) { - return topics.mainPid; - }); + topics = topics.map(function(topic) { + return topic && topic.mainPid; + }).filter(Boolean); callback(null, topics); }); } diff --git a/src/sitemap.js b/src/sitemap.js index 5db793a128..288d75bb13 100644 --- a/src/sitemap.js +++ b/src/sitemap.js @@ -49,7 +49,7 @@ var path = require('path'), }, function(next) { var topicUrls = []; - topics.getTopicsFromSet(0, 'topics:recent', 0, -1, function(err, data) { + topics.getTopicsFromSet(0, 'topics:recent', 0, 49, function(err, data) { if (err) { return next(err); } diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 44bb228ade..aaecf9ad5d 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -14,11 +14,13 @@ var groups = require('../groups'), async = require('async'), winston = require('winston'), index = require('./index'), + cluster = require('cluster'), SocketAdmin = { user: require('./admin/user'), categories: require('./admin/categories'), groups: require('./admin/groups'), + tags: require('./admin/tags'), themes: {}, plugins: {}, widgets: {}, @@ -37,7 +39,19 @@ SocketAdmin.before = function(socket, method, next) { }); }; +SocketAdmin.reload = function(socket, data, callback) { + events.logWithUser(socket.uid, ' is reloading NodeBB'); + if (cluster.isWorker) { + process.send({ + action: 'reload' + }); + } else { + meta.reload(callback); + } +}; + SocketAdmin.restart = function(socket, data, callback) { + events.logWithUser(socket.uid, ' is restarting NodeBB'); meta.restart(); }; diff --git a/src/socket.io/admin/tags.js b/src/socket.io/admin/tags.js new file mode 100644 index 0000000000..08ceca1a9f --- /dev/null +++ b/src/socket.io/admin/tags.js @@ -0,0 +1,15 @@ +"use strict"; + +var topics = require('../../topics'), + Tags = {}; + +Tags.update = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + topics.updateTag(data.tag, data, callback); +}; + + +module.exports = Tags; \ No newline at end of file diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index bdd3d2566d..b861806df1 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -76,11 +76,7 @@ User.banUser = function(uid, callback) { return callback(err); } - var sockets = websockets.getUserSockets(uid); - - for(var i=0; i 0, logger.io_one will hook into this socket */ logger.io_one(socket, uid); if (uid) { - - db.sortedSetAdd('users:online', Date.now(), uid, function(err, data) { - socket.join('uid_' + uid); - + socket.join('uid_' + uid); + socket.join('online_users'); + db.sortedSetAdd('users:online', Date.now(), uid, function(err) { async.parallel({ user: function(next) { - user.getUserFields(uid, ['username', 'userslug'], next); + user.getUserFields(uid, ['username', 'userslug', 'picture', 'status'], next); }, isAdmin: function(next) { user.isAdministrator(uid, next); } }, function(err, userData) { + if (err || !userData.user) { + return; + } socket.emit('event:connect', { status: 1, username: userData.user.username, userslug: userData.user.userslug, + picture: userData.user.picture, isAdmin: userData.isAdmin, uid: uid }); - socketUser.isOnline(socket, uid, function(err, data) { - socket.broadcast.emit('user.isOnline', err, data); - }); + socket.broadcast.emit('event:user_status_change', {uid:uid, status: userData.user.status}); }); }); } else { - socket.broadcast.emit('user.anonConnect'); + socket.join('online_guests'); socket.emit('event:connect', { status: 1, username: '[[global:guest]]', @@ -111,33 +174,30 @@ Sockets.init = function(server) { }); socket.on('disconnect', function() { - - if (uid && Sockets.getUserSockets(uid).length <= 1) { + var socketCount = Sockets.getUserSocketCount(uid); + if (uid && socketCount <= 1) { db.sortedSetRemove('users:online', uid, function(err) { - socketUser.isOnline(socket, uid, function(err, data) { - socket.broadcast.emit('user.isOnline', err, data); - }); + if (err) { + return winston.error(err.message); + } + socket.broadcast.emit('event:user_status_change', {uid: uid, status: 'offline'}); }); } - if (!uid) { - socket.broadcast.emit('user.anonDisconnect'); - } + onUserDisconnect(uid, socket.id, socketCount); emitOnlineUserCount(); for(var roomName in io.sockets.manager.roomClients[socket.id]) { - updateRoomBrowsingText(roomName.slice(1)); + if (roomName.indexOf('topic') !== -1) { + io.sockets.in(roomName.slice(1)).emit('event:user_leave', socket.uid); + } } }); socket.on('*', function(payload, callback) { function callMethod(method) { - if(socket.uid) { - user.updateLastOnlineTime(socket.uid); - } - method.call(null, socket, payload.args.length ? payload.args[0] : null, function(err, result) { if (callback) { callback(err?{message:err.message}:null, result); @@ -150,7 +210,7 @@ Sockets.init = function(server) { } var parts = payload.name.toString().split('.'), - namespace = parts.slice(0, 1), + namespace = parts[0], methodToCall = parts.reduce(function(prev, cur) { if (prev !== null && prev[cur]) { return prev[cur]; @@ -196,38 +256,61 @@ Sockets.in = function(room) { }; Sockets.uidInRoom = function(uid, room) { - var clients = io.sockets.clients(room); + var userSocketIds = io.sockets.manager.rooms['/uid_' + uid]; + if (!Array.isArray(userSocketIds) || !userSocketIds.length) { + return false; + } - uid = parseInt(uid, 10); + var roomSocketIds = io.sockets.manager.rooms['/' + room]; + if (!Array.isArray(roomSocketIds) || !roomSocketIds.length) { + return false; + } - if (typeof uid === 'number' && uid > 0) { - clients = clients.filter(function(socketObj) { - return uid === socketObj.uid; - }); + for (var i=0; i 0; + return Array.isArray(io.sockets.manager.rooms['/uid_' + uid]); } +Sockets.isUsersOnline = function(uids, callback) { + var data = uids.map(isUserOnline); + + callback(null, data); +}; + Sockets.updateRoomBrowsingText = updateRoomBrowsingText; -function updateRoomBrowsingText(roomName) { +function updateRoomBrowsingText(roomName, selfUid) { if (!roomName) { return; } - function getAnonymousCount() { - var clients = io.sockets.clients(roomName); - var anonCount = 0; - - for (var i = 0; i < clients.length; ++i) { - if(clients[i].uid === 0) { - ++anonCount; - } - } - return anonCount; + var uids = Sockets.getUidsInRoom(roomName); + uids = uids.slice(0, 9); + if (selfUid) { + uids = [selfUid].concat(uids); + } + if (!uids.length) { + return; } - - var uids = Sockets.getUidsInRoom(roomName), - anonymousCount = getAnonymousCount(); - user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status'], function(err, users) { if(!err) { users = users.filter(function(user) { @@ -310,7 +400,6 @@ function updateRoomBrowsingText(roomName) { io.sockets.in(roomName).emit('event:update_users_in_room', { users: users, - anonymousCount: anonymousCount, room: roomName }); } @@ -319,16 +408,26 @@ function updateRoomBrowsingText(roomName) { Sockets.getUidsInRoom = function(roomName) { var uids = []; - var clients = io.sockets.clients(roomName); - for(var i=0; i 6) { + otherCount = data[0].length - 5; + data[0] = data[0].slice(0, 5); } - - var pid_uids = data[0]; - - user.getUsernamesByUids(pid_uids, callback); + user.getUsernamesByUids(data[0], function(err, usernames) { + callback(err, { + otherCount: otherCount, + usernames: usernames + }); + }); }); }; -SocketPosts.getPidPage = function(socket, pid, callback) { - posts.getPidPage(pid, socket.uid, callback); -}; - -SocketPosts.getPidIndex = function(socket, pid, callback) { - posts.getPidIndex(pid, callback); -}; - SocketPosts.flag = function(socket, pid, callback) { if (!socket.uid) { return callback(new Error('[[error:not-logged-in]]')); } var message = '', - path = '', + userName = '', post; async.waterfall([ function(next) { - user.getUserField(socket.uid, 'username', next); + user.getUserFields(socket.uid, ['username', 'reputation'], next); }, - function(username, next) { - message = '[[notifications:user_flagged_post, ' + username + ']]'; + function(userData, next) { + if (parseInt(userData.reputation, 10) < parseInt(meta.config['privileges:flag'] || 1, 10)) { + return next(new Error('[[error:not-enough-reputation-to-flag]]')); + } + userName = userData.username; + posts.getPostFields(pid, ['tid', 'uid', 'content'], next); }, - function(postData, next) { - postTools.parse(postData.content, function(err, parsed) { - postData.content = parsed; - next(undefined, postData); - }); - }, function(postData, next) { post = postData; - topics.getTopicField(postData.tid, 'slug', next); + topics.getTopicField(postData.tid, 'title', next); }, - function(topicSlug, next) { - path = nconf.get('relative_path') + '/topic/' + topicSlug; - posts.getPidIndex(pid, next); + function(topicTitle, next) { + message = '[[notifications:user_flagged_post_in, ' + userName + ', ' + topicTitle + ']]'; + postTools.parse(post.content, next); }, - function(postIndex, next) { - path += '/' + postIndex; + function(postContent, next) { + post.content = postContent; groups.get('administrators', {}, next); }, function(adminGroup, next) { notifications.create({ bodyShort: message, bodyLong: post.content, - path: path, - uniqueId: 'post_flag:' + pid, + pid: pid, + nid: 'post_flag:' + pid + ':uid:' + socket.uid, from: socket.uid - }, function(err, nid) { - if (err) { + }, function(err, notification) { + if (err || !notification) { return next(err); } - notifications.push(nid, adminGroup.members, next); + notifications.push(notification, adminGroup.members, next); }); }, function(next) { @@ -376,4 +384,8 @@ SocketPosts.getCategory = function(socket, pid, callback) { posts.getCidByPid(pid, callback); }; +SocketPosts.getPidIndex = function(socket, pid, callback) { + posts.getPidIndex(pid, socket.uid, callback); +}; + module.exports = SocketPosts; diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index 4c66db8cb1..7b7b0fb7f2 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -1,22 +1,25 @@ 'use strict'; -var topics = require('../topics'), +var nconf = require('nconf'), + async = require('async'), + + topics = require('../topics'), categories = require('../categories'), privileges = require('../privileges'), + plugins = require('../plugins'), + notifications = require('../notifications'), threadTools = require('../threadTools'), websockets = require('./index'), user = require('../user'), - db = require('./../database'), - meta = require('./../meta'), + db = require('../database'), + meta = require('../meta'), utils = require('../../public/src/utils'), - - async = require('async'), + SocketPosts = require('./posts'), SocketTopics = {}; SocketTopics.post = function(socket, data, callback) { - if(!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -30,27 +33,34 @@ SocketTopics.post = function(socket, data, callback) { tags: data.tags, req: websockets.reqFromSocket(socket) }, function(err, result) { - if(err) { + if (err) { return callback(err); } - if (result) { + callback(null, result.topicData); + socket.emit('event:new_post', {posts: result.postData}); + socket.emit('event:new_topic', result.topicData); - websockets.server.sockets.in('category_' + data.category_id).emit('event:new_topic', result.topicData); - websockets.server.sockets.in('recent_posts').emit('event:new_topic', result.topicData); - websockets.server.sockets.in('home').emit('event:new_topic', result.topicData); - websockets.server.sockets.in('home').emit('event:new_post', { - posts: result.postData - }); - websockets.server.sockets.in('user/' + socket.uid).emit('event:new_post', { - posts: result.postData - }); + var uids = websockets.getConnectedClients(); + + privileges.categories.filterUids('read', result.topicData.cid, uids, function(err, uids) { + if (err) { + return; + } - module.parent.exports.emitTopicPostStats(); - topics.pushUnreadCount(); + plugins.fireHook('filter:sockets.sendNewPostToUids', {uidsTo: uids, uidFrom: data.uid, type: "newTopic"}, function(err, data) { + uids = data.uidsTo; - callback(null, result.topicData); - } + for(var i=0; iThemes ACP to modify and add widgets." + "html": Meta.config.motd || "Welcome to NodeBB, if you are an administrator of this forum visit the Themes ACP to modify and add widgets." } } ]), function(err) { @@ -171,7 +171,7 @@ Upgrade.upgrade = function(callback) { { "widget": "html", "data": { - "html": Meta.config['motd'] || "Welcome to NodeBB, if you are an administrator of this forum visit the Themes ACP to modify and add widgets.", + "html": Meta.config.motd || "Welcome to NodeBB, if you are an administrator of this forum visit the Themes ACP to modify and add widgets.", "container": container, "title": "MOTD" } @@ -963,6 +963,56 @@ Upgrade.upgrade = function(callback) { winston.info('[2014/7/24] Upgrading chats to sorted set - skipped'); next(); } + }, + function(next) { + thisSchemaDate = Date.UTC(2014, 8, 8); + + if (schemaDate < thisSchemaDate) { + winston.info('[2014/9/8] Deleting old notifications...'); + + async.parallel({ + uids: function(next) { + db.getSortedSetRange('users:joindate', 0, -1, next); + }, + nids: function(next) { + db.getSetMembers('notifications', next); + } + }, function(err, results) { + if (err) { + return next(err); + } + var uidKeys = results.uids.map(function(uid) { + return 'uid:' + uid + ':notifications:uniqueId:nid'; + }); + + var nidKeys = results.nids.filter(Boolean).map(function(nid) { + return 'notifications:' + nid; + }); + + async.series([ + function(next) { + db.deleteAll(nidKeys, next); + }, + function(next) { + db.deleteAll(uidKeys, next); + }, + function(next) { + db.delete('notifications', next); + } + ], function(err, results) { + if (err) { + winston.error('[2014/9/8] Error encountered while deleting notifications'); + return next(err); + } + + winston.info('[2014/9/8] Deleted old notifications'); + Upgrade.update(thisSchemaDate, next); + }); + }); + } else { + winston.info('[2014/9/8] Deleting old notifications skipped'); + next(); + } } // Add new schema updates here // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 22!!! diff --git a/src/user.js b/src/user.js index c0e96960c6..6cd8a125ea 100644 --- a/src/user.js +++ b/src/user.js @@ -1,7 +1,6 @@ 'use strict'; -var bcrypt = require('bcryptjs'), - async = require('async'), +var async = require('async'), nconf = require('nconf'), gravatar = require('gravatar'), @@ -9,7 +8,7 @@ var bcrypt = require('bcryptjs'), db = require('./database'), meta = require('./meta'), groups = require('./groups'), - emitter = require('./emitter'); + Password = require('./password'); (function(User) { @@ -68,9 +67,8 @@ var bcrypt = require('bcryptjs'), if (err) { return callback(err); } - plugins.fireHook('filter:user.removeFields', fieldsToRemove, function(err, fields) { - callback(err, modifyUserData(users, fields)); - }); + + modifyUserData(users, fieldsToRemove, callback); }); }; @@ -95,13 +93,11 @@ var bcrypt = require('bcryptjs'), return callback(err); } - plugins.fireHook('filter:user.removeFields', [], function(err, fields) { - callback(err, modifyUserData(users, fields)); - }); + modifyUserData(users, [], callback); }); }; - function modifyUserData(users, fieldsToRemove) { + function modifyUserData(users, fieldsToRemove, callback) { users.forEach(function(user) { if (!user) { return; @@ -113,6 +109,7 @@ var bcrypt = require('bcryptjs'), } if (!parseInt(user.uid, 10)) { + user.uid = 0; user.username = '[[global:guest]]'; user.userslug = ''; } @@ -131,13 +128,14 @@ var bcrypt = require('bcryptjs'), user[fieldsToRemove[i]] = undefined; } }); - return users; + + plugins.fireHook('filter:users.get', users, callback); } User.updateLastOnlineTime = function(uid, callback) { callback = callback || function() {}; - User.getUserField(uid, 'status', function(err, status) { - if(err || status === 'offline') { + User.getUserFields(uid, ['status', 'lastonline'], function(err, userData) { + if(err || userData.status === 'offline' || Date.now() - parseInt(userData.lastonline, 10) < 300000) { return callback(err); } @@ -152,7 +150,7 @@ var bcrypt = require('bcryptjs'), async.parallel({ userData: function(next) { - User.getUserFields(uid, ['banned', 'lastposttime', 'email', 'email:confirmed'], next); + User.getUserFields(uid, ['banned', 'lastposttime', 'joindate', 'email', 'email:confirmed'], next); }, exists: function(next) { db.exists('user:' + uid, next); @@ -182,13 +180,14 @@ var bcrypt = require('bcryptjs'), if (userData.email && parseInt(meta.config.requireEmailConfirmation, 10) === 1 && parseInt(userData['email:confirmed'], 10) !== 1) { return callback(new Error('[[error:email-not-confirmed]]')); } - - var lastposttime = userData.lastposttime; - if (!lastposttime) { - lastposttime = 0; + var now = Date.now(); + if (now - parseInt(userData.joindate, 10) < parseInt(meta.config.initialPostDelay, 10) * 1000) { + return callback(new Error('[[error:user-too-new, ' + meta.config.initialPostDelay + ']]')); } - if (Date.now() - parseInt(lastposttime, 10) < parseInt(meta.config.postDelay, 10) * 1000) { + var lastposttime = userData.lastposttime || 0; + + if (now - parseInt(lastposttime, 10) < parseInt(meta.config.postDelay, 10) * 1000) { return callback(new Error('[[error:too-many-posts, ' + meta.config.postDelay + ']]')); } callback(); @@ -278,7 +277,7 @@ var bcrypt = require('bcryptjs'), } var options = { - size: '128', + size: parseInt(meta.config.profileImageDimension, 10) || 128, default: customGravatarDefaultImage || meta.config.defaultGravatarImage || 'identicon', rating: 'pg' }; @@ -295,42 +294,39 @@ var bcrypt = require('bcryptjs'), return callback(null, password); } - bcrypt.genSalt(nconf.get('bcrypt_rounds'), function(err, salt) { - if (err) { - return callback(err); - } - bcrypt.hash(password, salt, callback); - }); + Password.hash(nconf.get('bcrypt_rounds'), password, callback); }; - User.onNewPostMade = function(postData) { - User.addPostIdToUser(postData.uid, postData.pid, postData.timestamp); - - User.incrementUserPostCountBy(postData.uid, 1); - - User.setUserField(postData.uid, 'lastposttime', postData.timestamp); + User.onNewPostMade = function(postData, callback) { + async.parallel([ + function(next) { + User.addPostIdToUser(postData.uid, postData.pid, postData.timestamp, next); + }, + function(next) { + User.incrementUserPostCountBy(postData.uid, 1, next); + }, + function(next) { + User.setUserField(postData.uid, 'lastposttime', postData.timestamp, next); + } + ], callback); }; - emitter.on('event:newpost', User.onNewPostMade); - User.incrementUserPostCountBy = function(uid, value, callback) { + callback = callback || function() {}; User.incrementUserFieldBy(uid, 'postcount', value, function(err, newpostcount) { if (err) { - if(typeof callback === 'function') { - callback(err); - } - return; + return callback(err); } db.sortedSetAdd('users:postcount', newpostcount, uid, callback); }); }; - User.addPostIdToUser = function(uid, pid, timestamp) { - db.sortedSetAdd('uid:' + uid + ':posts', timestamp, pid); + User.addPostIdToUser = function(uid, pid, timestamp, callback) { + db.sortedSetAdd('uid:' + uid + ':posts', timestamp, pid, callback); }; - User.addTopicIdToUser = function(uid, tid, timestamp) { - db.sortedSetAdd('uid:' + uid + ':topics', timestamp, tid); + User.addTopicIdToUser = function(uid, tid, timestamp, callback) { + db.sortedSetAdd('uid:' + uid + ':topics', timestamp, tid, callback); }; User.getPostIds = function(uid, start, stop, callback) { @@ -404,7 +400,11 @@ var bcrypt = require('bcryptjs'), }); groups.isMemberOfGroups(uid, groupNames, callback); } else { - groups.isMember(uid, 'cid:' + cid + ':privileges:mods', callback); + if (Array.isArray(uid)) { + groups.isMembers(uid, 'cid:' + cid + ':privileges:mods', callback); + } else { + groups.isMember(uid, 'cid:' + cid + ':privileges:mods', callback); + } } }; @@ -416,34 +416,22 @@ var bcrypt = require('bcryptjs'), } }; - User.isOnline = function(uids, callback) { - if (!Array.isArray(uids)) { - uids = [uids]; - } - - User.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'reputation', 'postcount'] , function(err, userData) { - if (err) { - return callback(err); - } - - var websockets = require('./socket.io'); - - userData = userData.map(function(user) { - var online = websockets.isUserOnline(user.uid); - user.status = online ? (user.status || 'online') : 'offline'; - - if (user.status === 'offline') { - online = false; - } + User.getIgnoredCategories = function(uid, callback) { + db.getSortedSetRange('uid:' + uid + ':ignored:cids', 0, -1, callback); + }; - user.online = online; - user.timestamp = Date.now(); - user.rooms = websockets.getUserRooms(user.uid); - return user; - }); + User.ignoreCategory = function(uid, cid, callback) { + if (!uid) { + return callback(); + } + db.sortedSetAdd('uid:' + uid + ':ignored:cids', Date.now(), cid, callback); + }; - callback(null, userData); - }); + User.watchCategory = function(uid, cid, callback) { + if (!uid) { + return callback(); + } + db.sortedSetRemove('uid:' + uid + ':ignored:cids', cid, callback); }; diff --git a/src/user/auth.js b/src/user/auth.js index b9c7b348ce..594e7e843e 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -1,6 +1,7 @@ 'use strict'; -var db = require('../database'), +var async = require('async'), + db = require('../database'), meta = require('../meta'); module.exports = function(User) { @@ -42,4 +43,11 @@ module.exports = function(User) { User.auth.clearLoginAttempts = function(uid) { db.delete('loginAttempts:' + uid); }; + + User.auth.resetLockout = function(uid, callback) { + async.parallel([ + async.apply(db.delete, 'loginAttempts:' + uid), + async.apply(db.delete, 'lockout:' + uid) + ], callback); + } }; \ No newline at end of file diff --git a/src/user/create.js b/src/user/create.js index 80a5877d7f..c6ecc17289 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -165,10 +165,11 @@ module.exports = function(User) { bodyShort: '[[user:username_taken_workaround, ' + userData.username + ']]', bodyLong: '', image: 'brand:logo', + nid: 'username_taken:' + uid, datetime: Date.now() - }, function(err, nid) { - if (!err) { - notifications.push(nid, uid); + }, function(err, notification) { + if (!err && notification) { + notifications.push(notification, uid); } }); } diff --git a/src/user/delete.js b/src/user/delete.js index 7a91603438..ac4ba1251d 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -6,8 +6,8 @@ var async = require('async'), user = require('../user'), topics = require('../topics'), groups = require('../groups'), - plugins = require('../plugins'); - + plugins = require('../plugins'), + batch = require('../batch'); module.exports = function(User) { @@ -20,11 +20,21 @@ module.exports = function(User) { deleteTopics(uid, next); }, function(next) { - deleteAccount(uid, next); + User.deleteAccount(uid, next); } ], callback); }; + User.deleteAccount = function(uid, callback) { + user.isAdministrator(uid, function(err, isAdmin) { + if (err || isAdmin) { + return callback(err || new Error('[[error:cant-ban-other-admins]]')); + } + + deleteAccount(uid, callback); + }); + }; + function deletePosts(uid, callback) { deleteSortedSetElements('uid:' + uid + ':posts', posts.purge, callback); } @@ -34,13 +44,13 @@ module.exports = function(User) { } function deleteSortedSetElements(set, deleteMethod, callback) { - db.getSortedSetRange(set, 0, -1, function(err, ids) { + batch.processSortedSet(set, function(err, ids, next) { if (err) { return callback(err); } - async.each(ids, deleteMethod, callback); - }); + async.eachLimit(ids, 10, deleteMethod, next); + }, {alwaysStartAt: 0}, callback); } function deleteAccount(uid, callback) { @@ -60,49 +70,18 @@ module.exports = function(User) { db.deleteObjectField('email:uid', userData.email.toLowerCase(), next); }, function(next) { - db.delete('uid:' + uid + ':notifications:read', next); - }, - function(next) { - db.delete('uid:' + uid + ':notifications:unread', next); - }, - function(next) { - db.delete('uid:' + uid + ':notifications:uniqueId:nid', next); - }, - function(next) { - db.sortedSetRemove('users:joindate', uid, next); - }, - function(next) { - db.sortedSetRemove('users:postcount', uid, next); + db.sortedSetsRemove(['users:joindate', 'users:postcount', 'users:reputation'], uid, next); }, function(next) { - db.sortedSetRemove('users:reputation', uid, next); - }, - function(next) { - db.delete('uid:' + uid + ':favourites', next); - }, - function(next) { - db.delete('user:' + uid + ':settings', next); - }, - function(next) { - db.delete('uid:' + uid + ':topics', next); - }, - function(next) { - db.delete('uid:' + uid + ':posts', next); - }, - function(next) { - db.delete('uid:' + uid + ':chats', next); - }, - function(next) { - db.delete('uid:' + uid + ':chats:unread', next); - }, - function(next) { - db.delete('uid:' + uid + ':ip', next); - }, - function(next) { - db.delete('uid:' + uid + ':upvote', next); - }, - function(next) { - db.delete('uid:' + uid + ':downvote', next); + var keys = [ + 'uid:' + uid + ':notifications:read', 'uid:' + uid + ':notifications:unread', + 'uid:' + uid + ':favourites', 'user:' + uid + ':settings', + 'uid:' + uid + ':topics', 'uid:' + uid + ':posts', + 'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread', + 'uid:' + uid + ':ip', 'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote', + 'uid:' + uid + ':ignored:cids' + ]; + db.deleteAll(keys, next); }, function(next) { deleteUserFromFollowers(uid, next); @@ -120,13 +99,7 @@ module.exports = function(User) { async.parallel([ function(next) { - db.delete('followers:' + uid, next); - }, - function(next) { - db.delete('following:' + uid, next); - }, - function(next) { - db.delete('user:' + uid, next); + db.deleteAll(['followers:' + uid, 'following:' + uid, 'user:' + uid], next); }, function(next) { db.decrObjectField('global', 'userCount', next); @@ -142,9 +115,11 @@ module.exports = function(User) { return callback(err); } - async.each(uids, function(theiruid, next) { - db.setRemove('following:' + theiruid, uid, next); - }, callback); + var sets = uids.map(function(uid) { + return 'following:' + uid; + }); + + db.setsRemove(sets, uid, callback); }); } }; diff --git a/src/user/email.js b/src/user/email.js index 892d740e0f..fe79771fbb 100644 --- a/src/user/email.js +++ b/src/user/email.js @@ -46,7 +46,7 @@ var async = require('async'), user.getUserField(uid, 'username', function(err, username) { if (err) { - return winston.error(err.message); + return winston.error(err.stack); } var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; diff --git a/src/user/jobs.js b/src/user/jobs.js index 160f3bb33c..d06a8386cd 100644 --- a/src/user/jobs.js +++ b/src/user/jobs.js @@ -69,6 +69,10 @@ module.exports = function(User) { winston.error('[user/jobs] Could not send daily digests: ' + err.message); return next(err); } + + // Remove expired notifications + notifications = notifications.filter(Boolean); + // Turn relative URLs into absolute ones for(var i=0; i maxNotifs) { - uniqueIds.length = maxNotifs; - } - - db.getObjectFields('uid:' + uid + ':notifications:uniqueId:nid', uniqueIds, function(err, uniqueIdToNids) { - if (err) { - return done(err); - } - - var nidsToUniqueIds = {}; - var nids = []; - uniqueIds.forEach(function(uniqueId) { - nidsToUniqueIds[uniqueIdToNids[uniqueId]] = uniqueId; - nids.push(uniqueIdToNids[uniqueId]); - }); - - async.map(nids, function(nid, next) { - notifications.get(nid, function(err, notif_data) { - if (err) { - return next(err); - } - - if (!notif_data) { - if (process.env.NODE_ENV === 'development') { - winston.info('[notifications.get] nid ' + nid + ' not found. Removing.'); - } - - db.sortedSetRemove(set, nidsToUniqueIds[nid]); - db.deleteObjectField('uid:' + uid + ':notifications:uniqueId:nid', nidsToUniqueIds[nid]); - return next(); - } - - if (typeof iterator === 'function') { - iterator(notif_data, next); - } else { - next(null, notif_data); - } - }); - }, done); - }); - }); - } - var maxNotifs = 15; async.parallel({ unread: function(next) { - getNotifications('uid:' + uid + ':notifications:unread', 0, 9, function(notif_data, next) { - notif_data.read = false; - notif_data.readClass = !notif_data.read ? 'label-warning' : ''; - next(null, notif_data); - }, next); + getNotificationsFromSet('uid:' + uid + ':notifications:unread', uid, 0, 9, maxNotifs, next); }, read: function(next) { - getNotifications('uid:' + uid + ':notifications:read', 0, 9, function(notif_data, next) { - notif_data.read = true; - next(null, notif_data); - }, next); + getNotificationsFromSet('uid:' + uid + ':notifications:read', uid, 0, 9, maxNotifs, next); } }, function(err, notifications) { - function filterDeleted(notifObj) { - return !!notifObj; - } - if (err) { return callback(err); } - notifications.read = notifications.read.filter(filterDeleted); - notifications.unread = notifications.unread.filter(filterDeleted); + notifications.read = notifications.read.filter(Boolean); + notifications.unread = notifications.unread.filter(Boolean); // Limit the number of notifications to `maxNotifs`, prioritising unread notifications if (notifications.read.length + notifications.unread.length > maxNotifs) { @@ -107,48 +44,67 @@ var async = require('async'), }); }; - UserNotifications.getAll = function(uid, limit, before, callback) { - var now = new Date(); - - if (!limit || parseInt(limit, 10) <= 0) { - limit = 25; - } - if (before) { - before = new Date(parseInt(before, 10)); - } - - db.getObjectValues('uid:' + uid + ':notifications:uniqueId:nid', function(err, nids) { + function getNotificationsFromSet(set, uid, start, stop, max, callback) { + db.getSortedSetRevRange(set, start, stop, function(err, nids) { if (err) { return callback(err); } - async.map(nids, function(nid, next) { - notifications.get(nid, function(err, notif_data) { - if (err || !notif_data) { - return next(err); - } - UserNotifications.isNotificationRead(notif_data.uniqueId, uid, function(err, isRead) { - if (err) { - return next(err); + if(!Array.isArray(nids) || !nids.length) { + return callback(null, []); + } + + if (nids.length > max) { + nids.length = max; + } + + UserNotifications.getNotifications(nids, uid, function(err, notifications) { + if (err) { + return callback(err); + } + + var deletedNids = []; + + notifications.forEach(function(notification, index) { + if (!notification) { + if (process.env.NODE_ENV === 'development') { + winston.info('[notifications.get] nid ' + nids[index] + ' not found. Removing.'); } - notif_data.read = isRead; - next(null, notif_data); - }); + deletedNids.push(nids[index]); + } }); - }, function(err, notifs) { + + if (deletedNids.length) { + db.sortedSetRemove(set, deletedNids); + } + + callback(null, notifications); + }); + }); + } + + UserNotifications.getAll = function(uid, count, callback) { + async.parallel({ + unread: function(next) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, count, next); + }, + read: function(next) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:read', 0, count, next); + } + }, function(err, results) { + if (err) { + return callback(err); + } + + var nids = results.unread.concat(results.read); + UserNotifications.getNotifications(nids, uid, function(err, notifs) { if (err) { return callback(err); } - notifs = notifs.filter(function(notif) { - return !!notif; - }).sort(function(a, b) { - return parseInt(b.datetime, 10) - parseInt(a.datetime, 10); - }).map(function(notif) { - notif.datetimeISO = utils.toISOString(notif.datetime); - notif.readClass = !notif.read ? 'label-warning' : ''; - return notif; + notifs = notifs.filter(Boolean).sort(function(a, b) { + return b.datetime - a.datetime; }); callback(null, notifs); @@ -156,73 +112,129 @@ var async = require('async'), }); }; - UserNotifications.isNotificationRead = function(uniqueId, uid, callback) { - db.isSortedSetMember('uid:' + uid + ':notifications:read', uniqueId, callback); + UserNotifications.getNotifications = function(nids, uid, callback) { + notifications.getMultiple(nids, function(err, notifications) { + if (err) { + return callback(err); + } + + db.isSortedSetMembers('uid:' + uid + ':notifications:read', nids, function(err, hasRead) { + if (err) { + return callback(err); + } + + var pids = notifications.map(function(notification) { + return notification ? notification.pid : null; + }); + + generatePostPaths(pids, uid, function(err, pidToPaths) { + if (err) { + return callback(err); + } + + notifications = notifications.map(function(notification, index) { + if (!notification) { + return null; + } + + notification.read = hasRead[index]; + notification.path = pidToPaths[notification.pid] || notification.path || ''; + notification.datetimeISO = utils.toISOString(notification.datetime); + notification.readClass = !notification.read ? 'label-warning' : ''; + return notification; + }); + + callback(null, notifications); + }); + }); + }); }; - UserNotifications.getDailyUnread = function(uid, callback) { - var now = Date.now(), - yesterday = now - (1000*60*60*24); // Approximate, can be more or less depending on time changes, makes no difference really. + function generatePostPaths(pids, uid, callback) { + pids = pids.filter(Boolean); + var postKeys = pids.map(function(pid) { + return 'post:' + pid; + }); - db.getSortedSetRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, yesterday, now, function(err, uniqueIds) { + db.getObjectsFields(postKeys, ['pid', 'tid'], function(err, postData) { if (err) { return callback(err); } - if (!Array.isArray(uniqueIds) || !uniqueIds.length) { - return callback(null, []); - } + var topicKeys = postData.map(function(post) { + return post ? 'topic:' + post.tid : null; + }); - db.getObjectFields('uid:' + uid + ':notifications:uniqueId:nid', uniqueIds, function(err, uniqueIdToNids) { + async.parallel({ + indices: function(next) { + posts.getPostIndices(postData, uid, next); + }, + topics: function(next) { + db.getObjectsFields(topicKeys, ['slug'], next); + } + }, function(err, results) { if (err) { return callback(err); } - var nids = Object.keys(uniqueIdToNids).map(function(uniqueId) { - return uniqueIdToNids[uniqueId]; + var pidToPaths = {}; + pids.forEach(function(pid, index) { + var slug = results.topics[index] ? results.topics[index].slug : null; + var postIndex = utils.isNumber(results.indices[index]) ? parseInt(results.indices[index], 10) + 1 : null; + + if (slug && postIndex) { + pidToPaths[pid] = nconf.get('relative_path') + '/topic/' + slug + '/' + postIndex; + } }); - async.map(nids, function(nid, next) { - notifications.get(nid, next); - }, callback); + callback(null, pidToPaths); }); }); + } + + UserNotifications.getDailyUnread = function(uid, callback) { + var now = Date.now(), + yesterday = now - (1000*60*60*24); // Approximate, can be more or less depending on time changes, makes no difference really. + + db.getSortedSetRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, yesterday, now, function(err, nids) { + if (err) { + return callback(err); + } + + if (!Array.isArray(nids) || !nids.length) { + return callback(null, []); + } + + UserNotifications.getNotifications(nids, uid, callback); + }); }; UserNotifications.getUnreadCount = function(uid, callback) { - db.sortedSetCount('uid:' + uid + ':notifications:unread', -Infinity, Infinity, callback); + db.sortedSetCard('uid:' + uid + ':notifications:unread', callback); }; UserNotifications.getUnreadByField = function(uid, field, value, callback) { - db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, -1, function(err, uniqueIds) { + db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, -1, function(err, nids) { if (err) { return callback(err); } - if (!Array.isArray(uniqueIds) || !uniqueIds.length) { + if (!Array.isArray(nids) || !nids.length) { return callback(null, []); } - db.getObjectFields('uid:' + uid + ':notifications:uniqueId:nid', uniqueIds, function(err, uniqueIdsToNids) { + UserNotifications.getNotifications(nids, uid, function(err, notifications) { if (err) { return callback(err); } - - var nids = Object.keys(uniqueIdsToNids).map(function(uniqueId) { - return uniqueIdsToNids[uniqueId]; + value = value ? value.toString() : ''; + nids = notifications.filter(function(notification) { + return notification && notification[field] !== value; + }).map(function(notification) { + return notification.nid; }); - async.filter(nids, function(nid, next) { - notifications.get(nid, function(err, notifObj) { - if (err || !notifObj) { - return next(false); - } - - next(notifObj[field] === value.toString()); - }); - }, function(nids) { - callback(null, nids); - }); + callback(null, nids); }); }); }; @@ -230,14 +242,13 @@ var async = require('async'), UserNotifications.sendPostNotificationToFollowers = function(uid, tid, pid) { db.getSetMembers('followers:' + uid, function(err, followers) { - if (err || !followers || !followers.length) { + if (err || !Array.isArray(followers) || !followers.length) { return; } async.parallel({ username: async.apply(user.getUserField, uid, 'username'), - topic: async.apply(topics.getTopicFields, tid, ['slug', 'cid', 'title']), - postIndex: async.apply(posts.getPidIndex, pid), + topic: async.apply(topics.getTopicFields, tid, ['cid', 'title']), postContent: function(next) { async.waterfall([ async.apply(posts.getPostField, pid, 'content'), @@ -258,23 +269,26 @@ var async = require('async'), return !results.topicFollowers[index]; }); - notifications.create({ - bodyShort: '[[notifications:user_posted_to, ' + results.username + ', ' + results.topic.title + ']]', - bodyLong: results.postContent, - path: nconf.get('relative_path') + '/topic/' + results.topic.slug + '/' + results.postIndex, - uniqueId: 'topic:' + tid + ':uid:' + uid, - tid: tid, - from: uid - }, function(err, nid) { - if (err) { + if (!followers.length) { + return; + } + + privileges.categories.filterUids('read', results.topic.cid, followers, function(err, followers) { + if (err || !followers.length) { return; } - async.filter(followers, function(uid, next) { - privileges.categories.can('read', results.topic.cid, uid, function(err, canRead) { - next(!err && canRead); - }); - }, function(followers){ - notifications.push(nid, followers); + + notifications.create({ + bodyShort: '[[notifications:user_posted_to, ' + results.username + ', ' + results.topic.title + ']]', + bodyLong: results.postContent, + pid: pid, + nid: 'tid:' + tid + ':pid:' + pid + ':uid:' + uid, + tid: tid, + from: uid + }, function(err, notification) { + if (!err && notification) { + notifications.push(notification, followers); + } }); }); }); @@ -285,7 +299,7 @@ var async = require('async'), var websockets = require('./../socket.io'); UserNotifications.getUnreadCount(uid, function(err, count) { if (err) { - return winston.warn('[User.pushNotifCount] Count not retrieve unread notifications count to push to uid ' + uid + '\'s client(s)'); + return winston.error(err.stack); } websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); diff --git a/src/user/profile.js b/src/user/profile.js index ea332dee21..e8d42da6d5 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -1,15 +1,15 @@ 'use strict'; -var bcrypt = require('bcryptjs'), - async = require('async'), +var async = require('async'), validator = require('validator'), S = require('string'), - utils = require('./../../public/src/utils'), - meta = require('./../meta'), - events = require('./../events'), - db = require('./../database'); + utils = require('../../public/src/utils'), + meta = require('../meta'), + events = require('../events'), + db = require('../database'), + Password = require('../password'); module.exports = function(User) { @@ -29,6 +29,10 @@ module.exports = function(User) { return next(); } + if (!utils.isEmailValid(data.email)) { + return next(new Error('[[error:invalid-email]]')); + } + User.getUserField(uid, 'email', function(err, email) { if(email === data.email) { return next(); @@ -53,6 +57,14 @@ module.exports = function(User) { return next(); } + if (data.username.length < meta.config.minimumUsernameLength) { + return next(new Error('[[error:username-too-short]]')); + } + + if (data.username.length > meta.config.maximumUsernameLength) { + return next(new Error('[[error:username-too-long]]')); + } + if(!utils.isUserNameValid(data.username) || !userslug) { return next(new Error('[[error:invalid-username]]')); } @@ -248,7 +260,7 @@ module.exports = function(User) { return hashAndSetPassword(callback); } - bcrypt.compare(data.currentPassword, currentPassword, function(err, res) { + Password.compare(data.currentPassword, currentPassword, function(err, res) { if (err || !res) { return callback(err || new Error('[[user:change_password_error_wrong_current]]')); } diff --git a/src/user/reset.js b/src/user/reset.js index ee80679bcd..2b101e8c27 100644 --- a/src/user/reset.js +++ b/src/user/reset.js @@ -88,10 +88,8 @@ var async = require('async'), db.deleteObjectField('reset:uid', code); db.deleteObjectField('reset:expiry', code); - db.delete('lockout:' + uid); - user.auth.clearLoginAttempts(uid); - callback(); + user.auth.resetLockout(uid, callback); }); }); }); diff --git a/src/user/settings.js b/src/user/settings.js index e023eda5eb..a98a050006 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -24,15 +24,19 @@ module.exports = function(User) { settings = data.settings; - settings.showemail = settings.showemail ? parseInt(settings.showemail, 10) !== 0 : false; - settings.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab ? parseInt(settings.openOutgoingLinksInNewTab, 10) !== 0 : false; + settings.showemail = parseInt(settings.showemail, 10) === 1; + settings.openOutgoingLinksInNewTab = parseInt(settings.openOutgoingLinksInNewTab, 10) === 1; settings.dailyDigestFreq = settings.dailyDigestFreq || 'off'; - 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; - settings.notificationSounds = settings.notificationSounds ? parseInt(settings.notificationSounds, 10) === 1 : true; + settings.usePagination = (settings.usePagination === null || settings.usePagination === undefined) ? parseInt(meta.config.usePagination, 10) === 1 : parseInt(settings.usePagination, 10) === 1; + settings.topicsPerPage = Math.min(settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : parseInt(meta.config.topicsPerPage, 10) || 20, 20); + settings.postsPerPage = Math.min(settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : parseInt(meta.config.postsPerPage, 10) || 10, 20); + settings.notificationSounds = parseInt(settings.notificationSounds, 10) === 1; settings.language = settings.language || meta.config.defaultLang || 'en_GB'; settings.topicPostSort = settings.topicPostSort || meta.config.topicPostSort || 'oldest_to_newest'; + settings.followTopicsOnCreate = (settings.followTopicsOnCreate === null || settings.followTopicsOnCreate === undefined) ? true : parseInt(settings.followTopicsOnCreate, 10) === 1; + settings.followTopicsOnReply = parseInt(settings.followTopicsOnReply, 10) === 1; + settings.sendChatNotifications = parseInt(settings.sendChatNotifications, 10) === 1; + callback(null, settings); }); }); @@ -64,7 +68,6 @@ module.exports = function(User) { }; 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]]')); } @@ -77,10 +80,13 @@ module.exports = function(User) { openOutgoingLinksInNewTab: data.openOutgoingLinksInNewTab, dailyDigestFreq: data.dailyDigestFreq || 'off', usePagination: data.usePagination, - topicsPerPage: data.topicsPerPage, - postsPerPage: data.postsPerPage, + topicsPerPage: Math.min(data.topicsPerPage, 20), + postsPerPage: Math.min(data.postsPerPage, 20), notificationSounds: data.notificationSounds, - language: data.language || meta.config.defaultLang + language: data.language || meta.config.defaultLang, + followTopicsOnCreate: data.followTopicsOnCreate, + followTopicsOnReply: data.followTopicsOnReply, + sendChatNotifications: data.sendChatNotifications }, callback); }; diff --git a/src/webserver.js b/src/webserver.js index 9b50f0b553..d9d47d7f3c 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -6,6 +6,7 @@ var path = require('path'), server, winston = require('winston'), async = require('async'), + cluster = require('cluster'), emailer = require('./emailer'), db = require('./database'), @@ -36,16 +37,21 @@ if(nconf.get('ssl')) { var port = nconf.get('PORT') || nconf.get('port'); logger.init(app); - auth.registerApp(app); emailer.registerApp(app); - notifications.init(); - user.startJobs(); + + if (cluster.isWorker && process.env.handle_jobs === 'true') { + notifications.init(); + user.startJobs(); + } // Preparation dependent on plugins plugins.ready(function() { meta.js.minify(app.enabled('minification')); meta.css.minify(); - meta.sounds.init(); + + if (cluster.isWorker && process.env.cluster_setup === 'true') { + meta.sounds.init(); + } }); async.parallel({ @@ -96,11 +102,15 @@ if(nconf.get('ssl')) { } module.exports.server = server; - module.exports.init = function () { + module.exports.init = function(callback) { server.on("error", function(err){ if (err.code === 'EADDRINUSE') { winston.error('NodeBB address in use, exiting...'); - process.exit(1); + if (cluster.isWorker) { + cluster.worker.kill(); + } else { + process.exit(0); + } } else { throw err; } @@ -109,21 +119,29 @@ if(nconf.get('ssl')) { emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function() { winston.info('NodeBB Ready'); emitter.emit('nodebb:ready'); + emitter.removeAllListeners('templates:compiled').removeAllListeners('meta:js.compiled').removeAllListeners('meta:css.compiled'); }); - emitter.on('templates:compiled', function() { - var bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port; - winston.info('NodeBB attempting to listen on: ' + bind_address); - - server.listen(port, nconf.get('bind_address'), function(){ - winston.info('NodeBB is now listening on: ' + bind_address); - if (process.send) { - process.send({ - action: 'ready', - bind_address: bind_address - }); - } - }); + if (process.send) { + callback(); + } else { + module.exports.listen(); + } + }; + + module.exports.listen = function() { + var bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port; + winston.info('NodeBB attempting to listen on: ' + bind_address); + + server.listen(port, nconf.get('bind_address'), function() { + winston.info('NodeBB is now listening on: ' + bind_address); + if (process.send) { + process.send({ + action: 'listening', + bind_address: bind_address, + primary: process.env.handle_jobs === 'true' + }); + } }); }; diff --git a/src/widgets.js b/src/widgets.js index b04101fee5..1755ab9cb5 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -3,57 +3,81 @@ var async = require('async'), winston = require('winston'), templates = require('templates.js'), - + plugins = require('./plugins'), db = require('./database'); (function(Widgets) { + Widgets.render = function(uid, area, callback) { - if (!area.location || !area.template) { + if (!area.locations || !area.template) { callback({ error: 'Missing location and template data' }); } - var rendered = []; + Widgets.getAreas(['global', area.template], area.locations, function(err, data) { - async.parallel({ - global: function(next) { - Widgets.getArea('global', area.location, next); - }, - local: function(next) { - Widgets.getArea(area.template, area.location, next); - } - }, function(err, data) { - var widgets = data.global.concat(data.local); + var widgetsByLocation = {}; + + async.map(area.locations, function(location, done) { + widgetsByLocation[location] = data.global[location].concat(data[area.template][location]); - async.eachSeries(widgets, function(widget, next) { - if (!!widget.data['registered-only'] && uid === 0) { - return next(); + if (!widgetsByLocation[location].length) { + return done(null, {location: location, widgets: []}); } - plugins.fireHook('filter:widget.render:' + widget.widget, { - uid: uid, - area: area, - data: widget.data - }, function(err, html){ - if (widget.data.container && widget.data.container.match('{body}')) { - html = templates.parse(widget.data.container, { - title: widget.data.title, - body: html - }); + async.map(widgetsByLocation[location], function(widget, next) { + + if (!widget || !widget.data || (!!widget.data['registered-only'] && uid === 0)) { + return next(); } - - rendered.push({ - html: html + + plugins.fireHook('filter:widget.render:' + widget.widget, { + uid: uid, + area: area, + data: widget.data + }, function(err, html) { + if (widget.data.container && widget.data.container.match('{body}')) { + html = templates.parse(widget.data.container, { + title: widget.data.title, + body: html + }); + } + + next(err, {html: html}); }); + }, function(err, widgets) { + done(err, {location: location, widgets: widgets.filter(Boolean)}); + }); + }, callback); + }); + }; + + Widgets.getAreas = function(templates, locations, callback) { + var keys = templates.map(function(tpl) { + return 'widgets:' + tpl; + }); + db.getObjectsFields(keys, locations, function(err, data) { + if (err) { + return callback(err); + } + + var returnData = {}; - next(err); + templates.forEach(function(template, index) { + returnData[template] = returnData[template] || {}; + locations.forEach(function(location) { + if (data && data[index] && data[index][location]) { + returnData[template][location] = JSON.parse(data[index][location]); + } else { + returnData[template][location] = []; + } }); - }, function(err) { - callback(err, rendered); }); + + callback(null, returnData); }); }; @@ -72,7 +96,7 @@ var async = require('async'), error: 'Missing location and template data' }); } - + db.setObjectField('widgets:' + area.template, area.location, JSON.stringify(area.widgets), function(err) { callback(err); }); diff --git a/tests/database.js b/tests/database.js index 5153bc0c8d..aa252243b4 100644 --- a/tests/database.js +++ b/tests/database.js @@ -271,14 +271,14 @@ describe('Test database', function() { it('should not throw err', function(done) { function sortedSetAdd(callback) { - db.sortedSetAdd('sortedSet3', 12, 5, function(err, data) { - callback(err, {'sortedSetAdd': data}); + db.sortedSetAdd('sortedSet3', 12, 5, function(err) { + callback(err); }); } function sortedSetRemove(callback) { db.sortedSetRemove('sortedSet3', 12, function(err, data) { - callback(err, {'sortedSetRemove': data}); + callback(err); }); }