diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000000..b1a381b64f --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,78 @@ +"use strict"; + +var fork = require('child_process').fork, + env = process.env, + worker, + incomplete = []; + + +module.exports = function(grunt) { + function update(action, filepath, target) { + var args = [], + fromFile = '', + compiling = '', + time = Date.now(); + + if (!grunt.option('verbose')) { + args.push('--log-level=info'); + } + + if (target === 'lessUpdated') { + fromFile = ['js','tpl']; + compiling = 'less'; + } else if (target === 'clientUpdated') { + fromFile = ['less','tpl']; + compiling = 'js'; + } else if (target === 'templatesUpdated') { + fromFile = ['js','less']; + compiling = 'tpl'; + } else if (target === 'serverUpdated') { + fromFile = ['less','js','tpl']; + } + + fromFile = fromFile.filter(function(ext) { + return incomplete.indexOf(ext) === -1; + }); + + args.push('--from-file=' + fromFile.join(',')); + incomplete.push(compiling); + + worker.kill(); + worker = fork('app.js', args, { env: env }); + + worker.on('message', function() { + if (incomplete.length) { + incomplete = []; + + if (grunt.option('verbose')) { + grunt.log.writeln('NodeBB restarted in ' + (Date.now() - time) + ' ms'); + } + } + }); + } + + grunt.initConfig({ + watch: { + lessUpdated: { + files: ['public/**/*.less', 'node_modules/nodebb-*/*.less', 'node_modules/nodebb-*/*/*.less', 'node_modules/nodebb-*/*/*/*.less', 'node_modules/nodebb-*/*/*/*/*.less'] + }, + clientUpdated: { + files: ['public/src/**/*.js', 'node_modules/nodebb-*/*.js', 'node_modules/nodebb-*/*/*.js', 'node_modules/nodebb-*/*/*/*.js', 'node_modules/nodebb-*/*/*/*/*.js'] + }, + serverUpdated: { + files: ['*.js', 'src/**/*.js'] + }, + templatesUpdated: { + files: ['src/views/**/*.tpl', 'node_modules/nodebb-*/*.tpl', 'node_modules/nodebb-*/*/*.tpl', 'node_modules/nodebb-*/*/*/*.tpl', 'node_modules/nodebb-*/*/*/*/*.tpl'] + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.registerTask('default', ['watch']); + + env.NODE_ENV = 'development'; + + worker = fork('app.js', [], { env: env }); + grunt.event.on('watch', update); +}; \ No newline at end of file diff --git a/app.js b/app.js index dcdf8e1819..ef113e5ece 100644 --- a/app.js +++ b/app.js @@ -43,7 +43,7 @@ winston.add(winston.transports.Console, { var date = new Date(); return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; }, - level: global.env === 'production' ? 'info' : 'verbose' + level: (global.env === 'production' || nconf.get('log-level') === 'info') ? 'info' : 'verbose' }); if(os.platform() === 'linux') { @@ -323,7 +323,7 @@ function resetThemes(callback) { function resetPlugin(pluginId) { var db = require('./src/database'); - db.setRemove('plugins:active', pluginId, function(err) { + db.sortedSetRemove('plugins:active', pluginId, function(err) { if (err) { winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err.message); } else { diff --git a/bcrypt.js b/bcrypt.js index 7611ab61a9..1445999a8a 100644 --- a/bcrypt.js +++ b/bcrypt.js @@ -13,6 +13,7 @@ process.on('message', function(msg) { } }); + function hashPassword(password, rounds) { async.waterfall([ function(next) { diff --git a/nodebb b/nodebb index e9922f8007..2f4f38a4be 100755 --- a/nodebb +++ b/nodebb @@ -110,9 +110,10 @@ case "$1" in ;; watch) - echo "Launching NodeBB in \"development\" mode." - echo "To run the production build of NodeBB, please use \"forever\"." - echo "More Information: https://docs.nodebb.org/en/latest/running/index.html" + echo "***************************************************************************" + echo "WARNING: ./nodebb watch will be deprecated soon. Please use grunt: " + echo "https://docs.nodebb.org/en/latest/running/index.html#grunt-development" + echo "***************************************************************************" NODE_ENV=development supervisor -q --ignore public/templates,public/nodebb.min.js,public/nodebb.min.js.map --extensions 'node|js|tpl|less' -- app "$@" ;; diff --git a/package.json b/package.json index a7907757e8..93c685afec 100644 --- a/package.json +++ b/package.json @@ -33,18 +33,21 @@ "heapdump": "^0.3.0", "less": "^2.0.0", "logrotate-stream": "^0.2.3", + "mime": "^1.3.4", "mkdirp": "~0.5.0", + "mmmagic": "^0.3.13", "morgan": "^1.3.2", "nconf": "~0.7.1", "nodebb-plugin-dbsearch": "^0.1.0", "nodebb-plugin-emoji-extended": "^0.4.1-4", - "nodebb-plugin-markdown": "^0.8.0", + "nodebb-plugin-markdown": "^1.0.0", "nodebb-plugin-mentions": "^0.9.0", "nodebb-plugin-soundpack-default": "~0.1.1", "nodebb-plugin-spam-be-gone": "^0.4.0", - "nodebb-theme-lavender": "^1.0.0", - "nodebb-theme-vanilla": "^1.0.0", - "nodebb-widget-essentials": "~0.2.0", + "nodebb-theme-lavender": "^1.0.6", + "nodebb-theme-vanilla": "^1.0.14", + "nodebb-widget-essentials": "~0.2.12", + "nodebb-rewards-essentials": "^0.0.1", "npm": "^2.1.4", "passport": "^0.2.1", "passport-local": "1.0.0", @@ -68,7 +71,9 @@ "xregexp": "~2.0.0" }, "devDependencies": { - "mocha": "~1.13.0" + "mocha": "~1.13.0", + "grunt": "~0.4.5", + "grunt-contrib-watch": "^0.6.1" }, "bugs": { "url": "https://github.com/NodeBB/NodeBB/issues" diff --git a/public/language/ar/category.json b/public/language/ar/category.json index 27dad61d69..b4f3a40f7a 100644 --- a/public/language/ar/category.json +++ b/public/language/ar/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "موضوع جديد", + "guest-login-post": "Log in to post", "no_topics": "لا توجد مواضيع في هذه الفئةلم لا تحاول إنشاء موضوع؟
", "browsing": "تصفح", "no_replies": "لم يرد أحد", diff --git a/public/language/ar/topic.json b/public/language/ar/topic.json index 256a6d2b0c..c63f32e61c 100644 --- a/public/language/ar/topic.json +++ b/public/language/ar/topic.json @@ -12,6 +12,7 @@ "notify_me": "تلق تنبيهات بالردود الجديدة في هذا الموضوع", "quote": "اقتبس", "reply": "رد", + "guest-login-reply": "Log in to reply", "edit": "تعديل", "delete": "حذف", "purge": "تطهير", diff --git a/public/language/bn/category.json b/public/language/bn/category.json index 82626ebe86..b706e85080 100644 --- a/public/language/bn/category.json +++ b/public/language/bn/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "নতুন টপিক", + "guest-login-post": "Log in to post", "no_topics": "এই বিভাগে কোন টপিক নেই!
আপনি চাইলে একটি পোষ্ট করতে পারেন।", "browsing": "ব্রাউজিং", "no_replies": "কোন রিপ্লাই নেই", diff --git a/public/language/bn/topic.json b/public/language/bn/topic.json index e7a0b35aa0..3b0835cf2e 100644 --- a/public/language/bn/topic.json +++ b/public/language/bn/topic.json @@ -12,6 +12,7 @@ "notify_me": "এই টপিকে নতুন উত্তর আসলে জানুন", "quote": "উদ্ধৃতি", "reply": "উত্তর", + "guest-login-reply": "Log in to reply", "edit": "সম্পাদণা", "delete": "মুছে ফেলুন", "purge": "পার্জ", diff --git a/public/language/cs/category.json b/public/language/cs/category.json index 92d0e10df2..0d598e974a 100644 --- a/public/language/cs/category.json +++ b/public/language/cs/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nové téma", + "guest-login-post": "Log in to post", "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", diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json index 9b18a9ba03..4f20b6e6db 100644 --- a/public/language/cs/topic.json +++ b/public/language/cs/topic.json @@ -12,6 +12,7 @@ "notify_me": "Sledovat toto téma", "quote": "Citovat", "reply": "Odpovědět", + "guest-login-reply": "Log in to reply", "edit": "Upravit", "delete": "Smazat", "purge": "Purge", diff --git a/public/language/de/category.json b/public/language/de/category.json index e3d45d3af8..f6a13932c2 100644 --- a/public/language/de/category.json +++ b/public/language/de/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Neues Thema", + "guest-login-post": "Anmelden um einen Beitrag zu erstellen", "no_topics": "Es gibt noch keine Themen in dieser Kategorie.
Warum beginnst du nicht eins?", "browsing": "Aktiv", "no_replies": "Niemand hat geantwortet", diff --git a/public/language/de/groups.json b/public/language/de/groups.json index 375e09d9b5..92b805dd6a 100644 --- a/public/language/de/groups.json +++ b/public/language/de/groups.json @@ -15,7 +15,7 @@ "details.latest_posts": "Aktuelle Beiträge", "details.private": "Private Gruppe", "details.public": "Öffentliche Gruppe", - "details.grant": "Grant/Rescind Ownership", + "details.grant": "Gewähre/Widerrufe Besitz", "details.kick": "Kick", "details.owner_options": "Gruppenadministration", "event.updated": "Gruppendetails wurden aktualisiert", diff --git a/public/language/de/topic.json b/public/language/de/topic.json index e2e47d83c1..7036cb823a 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -12,6 +12,7 @@ "notify_me": "Erhalte eine Benachrichtigung bei neuen Antworten zu diesem Thema.", "quote": "zitieren", "reply": "antworten", + "guest-login-reply": "Anmelden zum Antworten", "edit": "bearbeiten", "delete": "löschen", "purge": "bereinigen", diff --git a/public/language/el/category.json b/public/language/el/category.json index 34cf7d16c3..73f3b51724 100644 --- a/public/language/el/category.json +++ b/public/language/el/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Νέο Θέμα", + "guest-login-post": "Log in to post", "no_topics": "Δεν υπάρχουν θέματα σε αυτή την κατηγορία.
Γιατί δεν δοκιμάζεις να δημοσιεύσεις ένα εσύ;", "browsing": "περιηγούνται", "no_replies": "Κανείς δεν έχει απαντήσει", diff --git a/public/language/el/topic.json b/public/language/el/topic.json index c8df981fa3..5e93f49db9 100644 --- a/public/language/el/topic.json +++ b/public/language/el/topic.json @@ -12,6 +12,7 @@ "notify_me": "Να ειδοποιούμαι για νέες απαντήσεις σε αυτό το θέμα", "quote": "Παράθεση", "reply": "Απάντηση", + "guest-login-reply": "Log in to reply", "edit": "Επεξεργασία", "delete": "Διαγραφή", "purge": "Εκκαθάριση", diff --git a/public/language/en@pirate/category.json b/public/language/en@pirate/category.json index 74b946d216..c2c0b64832 100644 --- a/public/language/en@pirate/category.json +++ b/public/language/en@pirate/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "New Topic", + "guest-login-post": "Log in to post", "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", diff --git a/public/language/en@pirate/topic.json b/public/language/en@pirate/topic.json index eb06a391c8..88cd2e2328 100644 --- a/public/language/en@pirate/topic.json +++ b/public/language/en@pirate/topic.json @@ -12,6 +12,7 @@ "notify_me": "Be notified of new replies in this topic", "quote": "Quote", "reply": "Reply", + "guest-login-reply": "Log in to reply", "edit": "Edit", "delete": "Delete", "purge": "Purge", diff --git a/public/language/en_GB/category.json b/public/language/en_GB/category.json index 22d72c3064..3e131894bd 100644 --- a/public/language/en_GB/category.json +++ b/public/language/en_GB/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "New Topic", + "guest-login-post": "Log in to post", "no_topics": "There are no topics in this category.
Why don't you try posting one?", "browsing": "browsing", diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index bf4e7229fd..b2f9a3b86b 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -24,6 +24,7 @@ "email-taken": "Email taken", "email-not-confirmed": "Your email has not been confirmed yet, please click here to confirm your email.", "email-not-confirmed-chat": "You are unable to chat until your email is confirmed", + "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "username-too-short": "Username too short", "username-too-long": "Username too long", @@ -64,6 +65,7 @@ "invalid-image-type": "Invalid image type. Allowed types are: %1", "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", "group-name-too-short": "Group name too short", "group-already-exists": "Group already exists", @@ -80,7 +82,6 @@ "topic-thumbnails-are-disabled": "Topic thumbnails are disabled.", "invalid-file": "Invalid File", "uploads-are-disabled": "Uploads are disabled", - "upload-error": "Upload Error : %1", "signature-too-long" : "Sorry, your signature cannot be longer than %1 characters.", @@ -96,5 +97,7 @@ "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.", "registration-error": "Registration Error", - "parse-error": "Something went wrong while parsing server response" + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login" } \ No newline at end of file diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index 964b5f42c6..a72cf20110 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -5,6 +5,9 @@ "new_group": "Create New Group", "no_groups_found": "There are no groups to see", + "pending.accept": "Accept", + "pending.reject": "Reject", + "cover-instructions": "Drag and Drop a photo, drag to position, and hit Save", "cover-change": "Change", "cover-save": "Save", @@ -15,12 +18,20 @@ "details.pending": "Pending Members", "details.has_no_posts": "This group's members have not made any posts.", "details.latest_posts": "Latest Posts", - "details.private": "Private Group", - "details.public": "Public Group", + "details.private": "Private", "details.grant": "Grant/Rescind Ownership", "details.kick": "Kick", "details.owner_options": "Group Administration", + "details.group_name": "Group Name", + "details.description": "Description", + "details.badge_preview": "Badge Preview", + "details.change_icon": "Change Icon", + "details.change_colour": "Change Colour", + "details.badge_text": "Badge Text", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Hidden", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", "event.updated": "Group details have been updated", "event.deleted": "The group \"%1\" has been deleted" diff --git a/public/language/en_GB/login.json b/public/language/en_GB/login.json index fe23e7d441..02abac6371 100644 --- a/public/language/en_GB/login.json +++ b/public/language/en_GB/login.json @@ -1,5 +1,7 @@ { - "username": "Username / Email", + "username-email": "Username / Email", + "username": "Username", + "email": "Email", "remember_me": "Remember Me?", "forgot_password": "Forgot Password?", "alternative_logins": "Alternative Logins", diff --git a/public/language/en_GB/notifications.json b/public/language/en_GB/notifications.json index 54c926a597..96aef60183 100644 --- a/public/language/en_GB/notifications.json +++ b/public/language/en_GB/notifications.json @@ -2,6 +2,7 @@ "title": "Notifications", "no_notifs": "You have no new notifications", "see_all": "See all Notifications", + "mark_all_read": "Mark all notifications read", "back_to_home": "Back to %1", "outgoing_link": "Outgoing Link", diff --git a/public/language/en_GB/recent.json b/public/language/en_GB/recent.json index f01670e47c..835dfce296 100644 --- a/public/language/en_GB/recent.json +++ b/public/language/en_GB/recent.json @@ -6,6 +6,7 @@ "year": "Year", "alltime": "All Time", "no_recent_topics": "There are no recent topics.", + "no_popular_topics": "There are no popular topics.", "there-is-a-new-topic": "There is a new topic.", "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index 5fc5863225..1bfdbb25ea 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -15,6 +15,7 @@ "notify_me": "Be notified of new replies in this topic", "quote": "Quote", "reply": "Reply", + "guest-login-reply": "Log in to reply", "edit": "Edit", "delete": "Delete", "purge": "Purge", diff --git a/public/language/en_US/category.json b/public/language/en_US/category.json index 93272e1a06..8245a2c365 100644 --- a/public/language/en_US/category.json +++ b/public/language/en_US/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "New Topic", + "guest-login-post": "Log in to post", "no_topics": "There are no topics in this category.
Why don't you try posting one?", "browsing": "browsing", "no_replies": "No one has replied", diff --git a/public/language/en_US/topic.json b/public/language/en_US/topic.json index 6a2e3d93c7..750ce2694a 100644 --- a/public/language/en_US/topic.json +++ b/public/language/en_US/topic.json @@ -12,6 +12,7 @@ "notify_me": "Be notified of new replies in this topic", "quote": "Quote", "reply": "Reply", + "guest-login-reply": "Log in to reply", "edit": "Edit", "delete": "Delete", "purge": "Purge", diff --git a/public/language/es/category.json b/public/language/es/category.json index 2bc1d330df..ec2fdadcdc 100644 --- a/public/language/es/category.json +++ b/public/language/es/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nuevo tema", + "guest-login-post": "Log in to post", "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", diff --git a/public/language/es/topic.json b/public/language/es/topic.json index f1a3f4221c..576220f99d 100644 --- a/public/language/es/topic.json +++ b/public/language/es/topic.json @@ -12,6 +12,7 @@ "notify_me": "Serás notificado cuando haya nuevas respuestas en este tema", "quote": "Citar", "reply": "Responder", + "guest-login-reply": "Log in to reply", "edit": "Editar", "delete": "Borrar", "purge": "Purgar", diff --git a/public/language/et/category.json b/public/language/et/category.json index 1c825ed2fe..d3c0f6e6bd 100644 --- a/public/language/et/category.json +++ b/public/language/et/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Uus teema", + "guest-login-post": "Log in to post", "no_topics": "Kahjuks ei leidu siin kategoorias ühtegi teemat.
Soovid postitada?", "browsing": "vaatab", "no_replies": "Keegi pole vastanud", diff --git a/public/language/et/topic.json b/public/language/et/topic.json index 2fdc0d8bf0..8992993fb9 100644 --- a/public/language/et/topic.json +++ b/public/language/et/topic.json @@ -12,6 +12,7 @@ "notify_me": "Saa teateid uutest postitustest selles teemas", "quote": "Tsiteeri", "reply": "Vasta", + "guest-login-reply": "Log in to reply", "edit": "Muuda", "delete": "Kustuta", "purge": "Kustuta", diff --git a/public/language/fa_IR/category.json b/public/language/fa_IR/category.json index c1c61a1a9f..b5b2749c70 100644 --- a/public/language/fa_IR/category.json +++ b/public/language/fa_IR/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "جستار تازه", + "guest-login-post": "Log in to post", "no_topics": "هیچ جستاری در این دسته نیست.
چرا شما یکی نفرستید؟", "browsing": "بیننده‌ها", "no_replies": "هیچ کسی پاسخ نداده است.", diff --git a/public/language/fa_IR/topic.json b/public/language/fa_IR/topic.json index 793af1e7cb..f523bee5b6 100644 --- a/public/language/fa_IR/topic.json +++ b/public/language/fa_IR/topic.json @@ -12,6 +12,7 @@ "notify_me": "از پاسخ‌های تازه در جستار آگاه شوید", "quote": "نقل قول", "reply": "پاسخ", + "guest-login-reply": "Log in to reply", "edit": "ویرایش", "delete": "Delete", "purge": "پاک کردن", diff --git a/public/language/fi/category.json b/public/language/fi/category.json index f3ae42f69f..3b098b3cfe 100644 --- a/public/language/fi/category.json +++ b/public/language/fi/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Uusi aihe", + "guest-login-post": "Log in to post", "no_topics": "Tällä aihealueella ei ole yhtään aihetta.
Miksi et aloittaisi uutta?", "browsing": "selaamassa", "no_replies": "Kukaan ei ole vastannut", diff --git a/public/language/fi/topic.json b/public/language/fi/topic.json index 194d414e56..eef05da8ee 100644 --- a/public/language/fi/topic.json +++ b/public/language/fi/topic.json @@ -12,6 +12,7 @@ "notify_me": "Ilmoita, kun tähän keskusteluun tulee uusia viestejä", "quote": "Lainaa", "reply": "Vastaa", + "guest-login-reply": "Log in to reply", "edit": "Muokkaa", "delete": "Poista", "purge": "Purge", diff --git a/public/language/fr/category.json b/public/language/fr/category.json index 63450150f5..98a8ae2b90 100644 --- a/public/language/fr/category.json +++ b/public/language/fr/category.json @@ -1,5 +1,6 @@ { - "new_topic_button": "Nouveau Sujet", + "new_topic_button": "Nouveau sujet", + "guest-login-post": "Se connecter pour poster", "no_topics": "Il n'y a aucun sujet dans cette catégorie.
Pourquoi ne pas en créer un ?", "browsing": "parcouru par", "no_replies": "Personne n'a répondu", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index bfa70c579c..3ceda0c847 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -17,7 +17,7 @@ "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, cliquer ici pour la valider.", + "email-not-confirmed": "Votre adresse email n'est pas confirmée, cliquez ici pour la valider.", "email-not-confirmed-chat": "Vous ne pouver discuter tant que votre email n'est pas confirmé", "username-too-short": "Nom d'utilisateur trop court", "username-too-long": "Nom d'utilisateur trop long", diff --git a/public/language/fr/global.json b/public/language/fr/global.json index bb53439c13..d09ab26cfc 100644 --- a/public/language/fr/global.json +++ b/public/language/fr/global.json @@ -3,7 +3,7 @@ "search": "Recherche", "buttons.close": "Fermer", "403.title": "Accès refusé", - "403.message": "Il semble que vous ayez atteint une page dont vous n'avez pas accès.", + "403.message": "Il semble que vous ayez atteint une page à laquelle vous n'avez pas accès.", "403.login": "Peut-être deviez vous essayer de vous connecter?", "404.title": "Introuvable", "404.message": "Il semble que vous ayez atteint une page qui n'existe pas. Retourner à la page d'accueil.", @@ -14,7 +14,7 @@ "please_log_in": "Veuillez vous connecter", "logout": "Déconnexion", "posting_restriction_info": "L'envoi de messages est réservé aux membres inscrits, cliquez ici pour vous connecter.", - "welcome_back": "Bon retour", + "welcome_back": "Bienvenue", "you_have_successfully_logged_in": "Vous vous êtes bien connecté", "save_changes": "Enregistrer les changements", "close": "Fermer", @@ -24,7 +24,7 @@ "header.admin": "Admin", "header.recent": "Récent", "header.unread": "Non lu", - "header.tags": "Tags", + "header.tags": "Mots-clés", "header.popular": "Populaire", "header.users": "Utilisateurs", "header.groups": "Groupes", @@ -32,7 +32,7 @@ "header.notifications": "Notifications", "header.search": "Recherche", "header.profile": "Profil", - "notifications.loading": "Chargement des Notifications", + "notifications.loading": "Chargement des notifications", "chats.loading": "Chargement des chats", "motd.welcome": "Bienvenue sur NodeBB, la plate-forme de discussion du futur.", "previouspage": "Page précédente", @@ -77,5 +77,5 @@ "privacy": "Vie privée", "follow": "S'abonner", "unfollow": "Se désabonner", - "delete_all": "Supprimer Tout" + "delete_all": "Tout supprimer" } \ No newline at end of file diff --git a/public/language/fr/groups.json b/public/language/fr/groups.json index d8c81570df..39a9efcff2 100644 --- a/public/language/fr/groups.json +++ b/public/language/fr/groups.json @@ -1,10 +1,10 @@ { "groups": "Groupes", "view_group": "Voir le groupe", - "owner": "Propriétaire du Groupe", - "new_group": "Créer un Nouveau Groupe", + "owner": "Propriétaire du groupe", + "new_group": "Créer un nouveau groupe", "no_groups_found": "Il n'y a aucun groupe", - "cover-instructions": "Glisser et Coller une image, ajuster la position, et cliquer sur Enregistrer", + "cover-instructions": "Glissez-déposez une image, ajustez la position, et cliquez sur Enregistrer", "cover-change": "Modifier", "cover-save": "Enregistrer", "cover-saving": "Enregistrement", @@ -13,11 +13,11 @@ "details.pending": "Membres en attente", "details.has_no_posts": "Les membres de ce groupe n'ont envoyé aucun message.", "details.latest_posts": "Derniers messages", - "details.private": "Groupe Privé", - "details.public": "Groupe Public", - "details.grant": "Grant/Rescind Ownership", - "details.kick": "Kick", - "details.owner_options": "Administration du Groupe", + "details.private": "Groupe privé", + "details.public": "Groupe public", + "details.grant": "Promouvoir/rétrograder comme propriétaire", + "details.kick": "Exclure", + "details.owner_options": "Administration du groupe", "event.updated": "Les détails du groupe ont été mis à jour", "event.deleted": "Le groupe é%1\" a été supprimé" } \ No newline at end of file diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json index 054c1c70f8..c472c6b402 100644 --- a/public/language/fr/modules.json +++ b/public/language/fr/modules.json @@ -1,11 +1,11 @@ { "chat.chatting_with": "Discuter avec ", - "chat.placeholder": "Taper votre message ici, appuyer sur Entrer pour envoyer", + "chat.placeholder": "Tapez votre message ici, appuyez sur Entrer pour envoyer", "chat.send": "Envoyer", "chat.no_active": "Vous n'avez aucune discussion en cours.", "chat.user_typing": "%1 est en train d'écrire ...", "chat.user_has_messaged_you": "%1 vous a envoyé un message.", - "chat.see_all": "Voir toutes les Discussions", + "chat.see_all": "Voir toutes les discussions", "chat.no-messages": "Veuillez sélectionner un destinataire pour voir l'historique des discussions", "chat.recent-chats": "Discussions récentes", "chat.contacts": "Contacts", diff --git a/public/language/fr/search.json b/public/language/fr/search.json index 20e22d2fa0..01574ac59b 100644 --- a/public/language/fr/search.json +++ b/public/language/fr/search.json @@ -29,7 +29,7 @@ "number-of-views": "Nombre de vues", "topic-start-date": "Date de création du sujet", "username": "Nom d'utilisateur", - "category": "Catgorie", + "category": "Catégorie", "descending": "Par ordre décroissant", "ascending": "Par ordre croissant" } \ No newline at end of file diff --git a/public/language/fr/tags.json b/public/language/fr/tags.json index 20bd5dccee..91a1f0aaa4 100644 --- a/public/language/fr/tags.json +++ b/public/language/fr/tags.json @@ -2,6 +2,6 @@ "no_tag_topics": "Il n'y a aucun sujet ayant ce mot-clé", "tags": "Mots-clés", "enter_tags_here": "Entrez les mots-clés ici. Appuyez sur entrer après chaque mot-clé.", - "enter_tags_here_short": "Entrez des tags...", + "enter_tags_here_short": "Entrez des mots-clés...", "no_tags": "Il n'y a pas encore de mots-clés." } \ No newline at end of file diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json index 135efce878..8248eb2580 100644 --- a/public/language/fr/topic.json +++ b/public/language/fr/topic.json @@ -12,7 +12,8 @@ "notify_me": "Être notifié des réponses dans ce sujet", "quote": "Citer", "reply": "Répondre", - "edit": "Editer", + "guest-login-reply": "Se connecter pour répondre", + "edit": "Éditer", "delete": "Supprimer", "purge": "Supprimer définitivement", "restore": "Restaurer", @@ -39,7 +40,7 @@ "share_this_post": "Partager ce message", "thread_tools.title": "Outils pour sujets", "thread_tools.markAsUnreadForAll": "Marquer comme non lu", - "thread_tools.pin": "Epingler le sujet", + "thread_tools.pin": "Épingler le sujet", "thread_tools.unpin": "Désépingler le sujet", "thread_tools.lock": "Verrouiller le sujet", "thread_tools.unlock": "Déverouiller le sujet", @@ -60,9 +61,9 @@ "disabled_categories_note": "Les catégories désactivées sont grisées", "confirm_move": "Déplacer", "confirm_fork": "Scinder", - "favourite": "Favoris", + "favourite": "Favori", "favourites": "Favoris", - "favourites.has_no_favourites": "Vous n'avez aucun favoris, mettez des messages en favoris pour les voir ici !", + "favourites.has_no_favourites": "Vous n'avez aucun favori, mettez des messages en favoris pour les voir ici !", "loading_more_posts": "Charger plus de messages", "move_topic": "Déplacer le sujet", "move_topics": "Déplacer des sujets", @@ -72,20 +73,20 @@ "topic_will_be_moved_to": "Ce sujet sera déplacé vers la catégorie", "fork_topic_instruction": "Cliquez sur les postes à scinder", "fork_no_pids": "Aucun post sélectionné !", - "fork_success": "Sujet copié avec succès! Cliquez ici pour aller au sujet copié.", - "composer.title_placeholder": "Entrer le titre du sujet ici ...", + "fork_success": "Sujet copié avec succès ! Cliquez ici pour aller au sujet copié.", + "composer.title_placeholder": "Entrer le titre du sujet ici…", "composer.handle_placeholder": "Nom", "composer.discard": "Abandonner", "composer.submit": "Envoyer", "composer.replying_to": "Réponse à %1", "composer.new_topic": "Nouveau sujet", - "composer.uploading": "envoi en cours ...", + "composer.uploading": "envoi en cours…", "composer.thumb_url_label": "Coller une URL de vignette du sujet", "composer.thumb_title": "Ajouter une vignette à ce sujet", "composer.thumb_url_placeholder": "http://exemple.com/vignette.png", "composer.thumb_file_label": "Ou envoyer un fichier", "composer.thumb_remove": "Effacer les champs", - "composer.drag_and_drop_images": "Glisser-déposer les images ici", + "composer.drag_and_drop_images": "Glissez-déposez les images ici", "more_users_and_guests": "%1 autre(s) utilisateur(s) et %2 invité(s)", "more_users": "%1 autre(s) utilisateur(s)", "more_guests": "%1 autre(s) invité(s)", diff --git a/public/language/fr/user.json b/public/language/fr/user.json index d2cf37feec..8786929c78 100644 --- a/public/language/fr/user.json +++ b/public/language/fr/user.json @@ -2,8 +2,8 @@ "banned": "Banni", "offline": "Hors-ligne", "username": "Nom d'utilisateur", - "joindate": "Date d'Adhésion", - "postcount": "Nombre de Messages", + "joindate": "Date d'adhésion", + "postcount": "Nombre de messages", "email": "Email", "confirm_email": "Confirmer l'adresse email", "delete_account": "Supprimer le compte", @@ -29,7 +29,7 @@ "unfollow": "Se désabonner", "profile_update_success": "Le profil a bien été mis à jour !", "change_picture": "Changer d'image", - "edit": "Editer", + "edit": "Éditer", "uploaded_picture": "Image envoyée", "upload_new_picture": "Envoyer une nouvelle image", "upload_new_picture_from_url": "Envoyer une nouvelle image depuis un URL", @@ -44,7 +44,7 @@ "confirm_password": "Confirmer le mot de passe", "password": "Mot de passe", "username_taken_workaround": "Le nom d'utilisateur désiré est déjà utilisé, nous l'avons donc légèrement modifié. Vous êtes maintenant connu comme %1", - "upload_picture": "Envoyer une image", + "upload_picture": "Envoyer l'image", "upload_a_picture": "Envoyer une image", "image_spec": "Vous ne pouvez envoyer que des fichiers PNG, JPG ou GIF", "max": "max.", diff --git a/public/language/fr/users.json b/public/language/fr/users.json index 1028292a9b..3467b5b864 100644 --- a/public/language/fr/users.json +++ b/public/language/fr/users.json @@ -6,7 +6,7 @@ "enter_username": "Entrer un nom d'utilisateur pour rechercher", "load_more": "Charger la suite", "users-found-search-took": "%1 utilisateur(s) trouvé(s)! La recherche a pris %2 secondes.", - "filter-by": "Filtrer Par", - "online-only": "En Ligne uniquement", - "picture-only": "Avec Image uniquement" + "filter-by": "Filtrer par", + "online-only": "En ligne uniquement", + "picture-only": "Avec image uniquement" } \ No newline at end of file diff --git a/public/language/he/category.json b/public/language/he/category.json index 86292076b6..66e87dd4e3 100644 --- a/public/language/he/category.json +++ b/public/language/he/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "נושא חדש", + "guest-login-post": "Log in to post", "no_topics": "קטגוריה זו ריקה מנושאים.
למה שלא תנסה להוסיף נושא חדש?", "browsing": "צופים בנושא זה כעת", "no_replies": "אין תגובות", diff --git a/public/language/he/topic.json b/public/language/he/topic.json index e16ebc08cf..3ff27d0a8a 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -12,6 +12,7 @@ "notify_me": "קבל התראה כאשר יש תגובות חדשות בנושא זה", "quote": "ציטוט", "reply": "תגובה", + "guest-login-reply": "Log in to reply", "edit": "עריכה", "delete": "מחק", "purge": "מחק הכל", diff --git a/public/language/hu/category.json b/public/language/hu/category.json index 6ea4699a4c..b36e9fc37a 100644 --- a/public/language/hu/category.json +++ b/public/language/hu/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Új témakör", + "guest-login-post": "Log in to post", "no_topics": "Nincs nyitva egy téma sem ebben a kategóriában.Hozzunk létre egyet.", "browsing": "böngészés", "no_replies": "Nem érkezett válasz", diff --git a/public/language/hu/pages.json b/public/language/hu/pages.json index f8d1c20484..53b98e8562 100644 --- a/public/language/hu/pages.json +++ b/public/language/hu/pages.json @@ -11,7 +11,7 @@ "user.followers": "Tagok akik követik %1 -t", "user.posts": "Hozzászólások által %1", "user.topics": "%1 által létrehozott témák", - "user.groups": "%1's Groups", + "user.groups": "%1's csoport", "user.favourites": "%1 Kedvenc Hozzászólásai", "user.settings": "Felhasználói Beállítások", "maintenance.text": "%1 jelenleg karbantartás alatt van. Kérlek nézz vissza késöbb!", diff --git a/public/language/hu/topic.json b/public/language/hu/topic.json index 0de62def8d..30bf3ab2f8 100644 --- a/public/language/hu/topic.json +++ b/public/language/hu/topic.json @@ -12,6 +12,7 @@ "notify_me": "Értesítést kérek az új hozzászólásokról ebben a topikban", "quote": "Idéz", "reply": "Válasz", + "guest-login-reply": "Log in to reply", "edit": "Szerkeszt", "delete": "Töröl", "purge": "Purge", diff --git a/public/language/hu/user.json b/public/language/hu/user.json index c673eaee36..bacce74415 100644 --- a/public/language/hu/user.json +++ b/public/language/hu/user.json @@ -2,8 +2,8 @@ "banned": "Kitíltva", "offline": "Nem elérhető", "username": "Felhasználónév", - "joindate": "Join Date", - "postcount": "Post Count", + "joindate": "Regisztráció dátum", + "postcount": "Bejegyzés megtekintés", "email": "E-mail", "confirm_email": "E-mail megerősítése", "delete_account": "Fiók törlése", @@ -18,7 +18,7 @@ "profile_views": "Megtekintések", "reputation": "Hírnév", "favourites": "Kedvencek", - "watched": "Watched", + "watched": "Megtekintve", "followers": "Követők", "following": "Követve", "signature": "Aláírás", diff --git a/public/language/id/category.json b/public/language/id/category.json index 89be56e6ff..7b0ff0f6eb 100644 --- a/public/language/id/category.json +++ b/public/language/id/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Topik Baru", + "guest-login-post": "Log in to post", "no_topics": "Tidak ada topik dikategori ini
Mengapa anda tidak mencoba membuat yang baru?", "browsing": "penjelajahan", "no_replies": "Belum ada orang yang menjawab", diff --git a/public/language/id/topic.json b/public/language/id/topic.json index cd8f9052b2..c47b9612be 100644 --- a/public/language/id/topic.json +++ b/public/language/id/topic.json @@ -12,6 +12,7 @@ "notify_me": "Beritahukan balasan baru untuk topik ini", "quote": "Kutip", "reply": "Balas", + "guest-login-reply": "Log in to reply", "edit": "Ubah", "delete": "Hapus", "purge": "Musnahkan", diff --git a/public/language/it/category.json b/public/language/it/category.json index b88bdd54e0..3e018bf12f 100644 --- a/public/language/it/category.json +++ b/public/language/it/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nuova Discussione", + "guest-login-post": "Log in to post", "no_topics": "Non ci sono discussioni in questa categoria.
Perché non ne inizi una?", "browsing": "visualizzando", "no_replies": "Nessuno ha risposto", diff --git a/public/language/it/search.json b/public/language/it/search.json index 9d021ec7c5..479fec6287 100644 --- a/public/language/it/search.json +++ b/public/language/it/search.json @@ -1,35 +1,35 @@ { "results_matching": "%1 risultato(i) corrispondente(i) \"%2\", (%3 secondi)", - "no-matches": "No matches found", + "no-matches": "Nessuna corrispondenza trovata", "in": "In", - "by": "By", - "titles": "Titles", - "titles-posts": "Titles and Posts", - "posted-by": "Posted by", - "in-categories": "In Categories", + "by": "Da", + "titles": "Titoli", + "titles-posts": "Titoli e Messaggi", + "posted-by": "Pubblicato da", + "in-categories": "In Categorie", "search-child-categories": "Search child categories", - "reply-count": "Reply Count", + "reply-count": "Numero Risposte", "at-least": "At least", "at-most": "At most", - "post-time": "Post time", - "newer-than": "Newer than", - "older-than": "Older than", - "any-date": "Any date", - "yesterday": "Yesterday", - "one-week": "One week", - "two-weeks": "Two weeks", - "one-month": "One month", - "three-months": "Three months", - "six-months": "Six months", - "one-year": "One year", - "sort-by": "Sort by", + "post-time": "Ora invio", + "newer-than": "Più nuovi di", + "older-than": "Più vecchi di", + "any-date": "Qualsiasi data", + "yesterday": "Ieri", + "one-week": "Una settimana", + "two-weeks": "Due settimane", + "one-month": "Un mese", + "three-months": "Tre mesi", + "six-months": "Sei mesi", + "one-year": "Un anno", + "sort-by": "Ordina per", "last-reply-time": "Last reply time", - "topic-title": "Topic title", - "number-of-replies": "Number of replies", - "number-of-views": "Number of views", - "topic-start-date": "Topic start date", - "username": "Username", - "category": "Category", - "descending": "In descending order", - "ascending": "In ascending order" + "topic-title": "Titolo argomento", + "number-of-replies": "Numero di risposte", + "number-of-views": "Numero di visite", + "topic-start-date": "Data inizio argomento", + "username": "Nome utente", + "category": "Categoria", + "descending": "In ordine decrescente", + "ascending": "In ordine crescente" } \ No newline at end of file diff --git a/public/language/it/topic.json b/public/language/it/topic.json index ba1beedc77..1cc385c23f 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -12,6 +12,7 @@ "notify_me": "Ricevi notifiche di nuove risposte in questa discussione", "quote": "Cita", "reply": "Rispondi", + "guest-login-reply": "Log in to reply", "edit": "Modifica", "delete": "Cancella", "purge": "Svuota", diff --git a/public/language/ja/category.json b/public/language/ja/category.json index c5ffc01c13..6fbd386dac 100644 --- a/public/language/ja/category.json +++ b/public/language/ja/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "新規スレッド", + "guest-login-post": "Log in to post", "no_topics": "まだスレッドはありません.
一番目のスレッドを書いてみないか?", "browsing": "閲覧中", "no_replies": "返事はまだありません", diff --git a/public/language/ja/topic.json b/public/language/ja/topic.json index fd6c7d911b..680b10b702 100644 --- a/public/language/ja/topic.json +++ b/public/language/ja/topic.json @@ -12,6 +12,7 @@ "notify_me": "このスレッドに新しいポストが投稿された際に通知する", "quote": "引用", "reply": "返答", + "guest-login-reply": "Log in to reply", "edit": "編集", "delete": "削除", "purge": "Purge", diff --git a/public/language/ko/category.json b/public/language/ko/category.json index 1ceaddb7ac..3518e3a3d1 100644 --- a/public/language/ko/category.json +++ b/public/language/ko/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "새 주제 생성", + "guest-login-post": "Log in to post", "no_topics": "이 카테고리에는 생성된 주제가 없습니다.
먼저 주제를 생성해 보세요.", "browsing": "이 주제를 읽고 있는 사용자", "no_replies": "답글이 없습니다.", diff --git a/public/language/ko/topic.json b/public/language/ko/topic.json index ed806c5882..56714d7a0a 100644 --- a/public/language/ko/topic.json +++ b/public/language/ko/topic.json @@ -12,6 +12,7 @@ "notify_me": "이 주제의 새 답글 알리기", "quote": "인용", "reply": "답글", + "guest-login-reply": "Log in to reply", "edit": "수정", "delete": "삭제", "purge": "폐기", diff --git a/public/language/lt/category.json b/public/language/lt/category.json index 1c9dd0899b..149a102553 100644 --- a/public/language/lt/category.json +++ b/public/language/lt/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nauja tema", + "guest-login-post": "Log in to post", "no_topics": "Šioje kategorijoje temų nėra.
Kodėl gi jums nesukūrus naujos?", "browsing": "naršo", "no_replies": "Niekas dar neatsakė", diff --git a/public/language/lt/topic.json b/public/language/lt/topic.json index 2df888e5b7..5138bc2766 100644 --- a/public/language/lt/topic.json +++ b/public/language/lt/topic.json @@ -12,6 +12,7 @@ "notify_me": "Gauti pranešimus apie naujus atsakymus šioje temoje", "quote": "Cituoti", "reply": "Atsakyti", + "guest-login-reply": "Log in to reply", "edit": "Redaguoti", "delete": "Ištrinti", "purge": "Išvalyti", diff --git a/public/language/ms/category.json b/public/language/ms/category.json index db2dc8d039..cc483ad763 100644 --- a/public/language/ms/category.json +++ b/public/language/ms/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "\nTopik Baru", + "guest-login-post": "Log in to post", "no_topics": "Tiada topik dalam kategori ini.
Cuba menghantar topik yang baru?", "browsing": "melihat", "no_replies": "Tiada jawapan", diff --git a/public/language/ms/topic.json b/public/language/ms/topic.json index 04be14258d..e173c79263 100644 --- a/public/language/ms/topic.json +++ b/public/language/ms/topic.json @@ -12,6 +12,7 @@ "notify_me": "Kekal dimaklumkan berkenaan respon dalam topik ini", "quote": "Petikan", "reply": "Balas", + "guest-login-reply": "Log in to reply", "edit": "Edit", "delete": "Padamkan", "purge": "Purge", diff --git a/public/language/nb/category.json b/public/language/nb/category.json index 7de0fb5cde..75f38d0df0 100644 --- a/public/language/nb/category.json +++ b/public/language/nb/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nytt emne", + "guest-login-post": "Log in to post", "no_topics": "Det er ingen emner i denne kategorien
Hvorfor ikke lage ett?", "browsing": "leser", "no_replies": "Ingen har svart", diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index 49096d9f6f..8a599b7942 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -12,6 +12,7 @@ "notify_me": "Bli varslet om nye svar i dette emnet", "quote": "Siter", "reply": "Svar", + "guest-login-reply": "Log in to reply", "edit": "Endre", "delete": "Slett", "purge": "Tøm", diff --git a/public/language/nl/category.json b/public/language/nl/category.json index 45dbbf4d4d..bee6530395 100644 --- a/public/language/nl/category.json +++ b/public/language/nl/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nieuw onderwerp", + "guest-login-post": "Log in to post", "no_topics": "Er zijn geen onderwerpen in deze categorie.
Waarom maak je er niet een aan?", "browsing": "verkennen", "no_replies": "Niemand heeft gereageerd", diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index 8ab45e30d3..5f9250bea4 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -12,6 +12,7 @@ "notify_me": "Krijg notificaties van nieuwe reacties op dit onderwerp", "quote": "Citeren", "reply": "Reageren", + "guest-login-reply": "Log in to reply", "edit": "Aanpassen", "delete": "Verwijderen", "purge": "weggooien", diff --git a/public/language/pl/category.json b/public/language/pl/category.json index fe8b77fe54..09164c69bb 100644 --- a/public/language/pl/category.json +++ b/public/language/pl/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nowy wątek", + "guest-login-post": "Log in to post", "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ł", diff --git a/public/language/pl/email.json b/public/language/pl/email.json index d1f327d90a..da76caf73c 100644 --- a/public/language/pl/email.json +++ b/public/language/pl/email.json @@ -9,9 +9,9 @@ "reset.text1": "Otrzymaliśmy żądanie przywrócenia Twojego hasła. Jeśli nie żądałeś przywrócenia hasła, zignoruj ten e-mail.", "reset.text2": "Aby przywrócić swoje hasło, skorzystaj z poniższego linku:", "reset.cta": "Kliknij tu, by przywrócić swoje hasło", - "reset.notify.subject": "Password successfully changed", - "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", - "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "reset.notify.subject": "Hasło pomyślnie zmienione", + "reset.notify.text1": "Informujemy, że %1, twoje hasło zostało pomyślnie zmienione.", + "reset.notify.text2": "Jeśli nie wyraziłeś na to zgody, proszę niezwłocznie poinformuj administratora.", "digest.notifications": "Masz nowe powiadomienia od %1:", "digest.latest_topics": "Ostatnie tematy z %1", "digest.cta": "Kliknij, by odwiedzić %1", @@ -20,8 +20,8 @@ "notif.chat.subject": "Nowa wiadomość czatu od %1", "notif.chat.cta": "Kliknij tutaj, by kontynuować konwersację", "notif.chat.unsub.info": "To powiadomienie o czacie zostało Ci wysłane zgodnie z ustawieniami Twojego konta.", - "notif.post.cta": "Click here to read the full topic", - "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.cta": "Kliknij tutaj, aby przeczytać cały wątek.", + "notif.post.unsub.info": "To powiadomienie o poście 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": "Kliknij tutaj, by zmienić te ustawienia", "closing": "Dziękujemy!" diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 119a9ca553..eaebbb6fcd 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -18,7 +18,7 @@ "username-taken": "Login zajęty.", "email-taken": "E-mail zajęty.", "email-not-confirmed": "Twój email nie został jeszcze potwierdzony. Proszę kliknąć tutaj by go potwierdzić.", - "email-not-confirmed-chat": "You are unable to chat until your email is confirmed", + "email-not-confirmed-chat": "Nie możesz rozmawiać do czasu, gdy twój email zostanie potwierdzony.", "username-too-short": "Nazwa użytkownika za krótka.", "username-too-long": "Zbyt długa nazwa użytkownika", "user-banned": "Użytkownik zbanowany", @@ -35,7 +35,7 @@ "topic-locked": "Temat zamknięty", "still-uploading": "Poczekaj na pełne załadowanie", "content-too-short": "Proszę wpisać dłuższy post. Posty powinny zawierać co najmniej %1 znaków.", - "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 characters.", + "content-too-long": "Proszę wpisać krótszy post. Posty nie mogą zawierać więcej niż %1 znaków.", "title-too-short": "Proszę podać dłuższy tytuł. Tytuły powinny zawierać co najmniej %1 znaków.", "title-too-long": "Wpisz krótszy tytuł, nie może być dłuższy niż %1 znaków.", "too-many-posts": "Możesz wysyłać posty co %1 sekund - proszę poczekać", @@ -45,13 +45,13 @@ "already-favourited": "Już polubiłeś ten post", "already-unfavourited": "Już przestałeś lubić ten post", "cant-ban-other-admins": "Nie możesz zbanować innych adminów!", - "invalid-image-type": "Invalid image type. Allowed types are: %1", - "invalid-image-extension": "Invalid image extension", + "invalid-image-type": "Błędny typ pliku. Dozwolone typy to: %1", + "invalid-image-extension": "Błędne rozszerzenie pliku", "group-name-too-short": "Nazwa grupy za krótka", "group-already-exists": "Grupa już istnieje", "group-name-change-not-allowed": "Nie można zmieniać nazwy tej grupy.", - "group-already-member": "You are already part of this group", - "group-needs-owner": "This group requires at least one owner", + "group-already-member": "Już należysz do tej grupy", + "group-needs-owner": "Ta grupa musi mieć przynajmniej jednego właściciela", "post-already-deleted": "Ten post został już skasowany", "post-already-restored": "Ten post został już przywrócony", "topic-already-deleted": "Ten temat został już skasowany", @@ -63,12 +63,12 @@ "signature-too-long": "Przepraszamy, ale podpis nie może być dłuższy niż %1 znaków.", "cant-chat-with-yourself": "Nie możesz rozmawiać ze sobą", "chat-restricted": "Ten użytkownik ograniczył swoje czaty. Musi Cię śledzić, zanim będziesz mógł z nim czatować.", - "too-many-messages": "You have sent too many messages, please wait awhile.", + "too-many-messages": "Wysłałeś zbyt wiele wiadomości, proszę poczekaj chwilę.", "reputation-system-disabled": "System reputacji został wyłączony", "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 dość reputacji, by flagować ten post", "reload-failed": "NodeBB napotkał problem w czasie ładowania \"%1\". Forum będzie nadal dostarczać zasoby dostępne w kliencie, jednak powinieneś cofnąć ostatnią akcję.", "registration-error": "Błąd rejestracji", - "parse-error": "Something went wrong while parsing server response" + "parse-error": "Coś poszło nie tak podczas parsingu odpowiedzi serwera" } \ No newline at end of file diff --git a/public/language/pl/global.json b/public/language/pl/global.json index a0c76caf7f..a12b45bcdf 100644 --- a/public/language/pl/global.json +++ b/public/language/pl/global.json @@ -3,10 +3,10 @@ "search": "Szukaj", "buttons.close": "Zamknij", "403.title": "Dostęp zabroniony", - "403.message": "You seem to have stumbled upon a page that you do not have access to.", - "403.login": "Perhaps you should try logging in?", + "403.message": "Wygląda na to, że trafiłeś na stronę, do której nie masz dostępu.", + "403.login": "Może powinieneś się zalogować?", "404.title": "Nie znaleziono", - "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "404.message": "Wygląda na to, że trafiłeś na stronę, która nie istnieje. Wróć do strony głównej.", "500.title": "Błąd wewnętrzny", "500.message": "Ups! Coś poszło nie tak.", "register": "Zarejestruj się", @@ -27,7 +27,7 @@ "header.tags": "Tagi", "header.popular": "Popularne", "header.users": "Użytkownicy", - "header.groups": "Groups", + "header.groups": "Grupy", "header.chats": "Rozmowy", "header.notifications": "Powiadomienia", "header.search": "Szukaj", @@ -75,7 +75,7 @@ "updated.title": "Forum zaktualizowane", "updated.message": "To forum zostało zaktualizowane do najnowszej wersji. Kliknij tutaj by odświeżyć stronę", "privacy": "Prywatność", - "follow": "Follow", - "unfollow": "Unfollow", + "follow": "Obserwuj", + "unfollow": "Przestań śledzić", "delete_all": "Usuń wszystko" } \ No newline at end of file diff --git a/public/language/pl/groups.json b/public/language/pl/groups.json index 0cd285b938..ca279d4b90 100644 --- a/public/language/pl/groups.json +++ b/public/language/pl/groups.json @@ -1,23 +1,23 @@ { "groups": "Grupy", "view_group": "Obejrzyj grupę", - "owner": "Group Owner", - "new_group": "Create New Group", - "no_groups_found": "There are no groups to see", - "cover-instructions": "Drag and Drop a photo, drag to position, and hit Save", - "cover-change": "Change", - "cover-save": "Save", - "cover-saving": "Saving", + "owner": "Właściciel grupy", + "new_group": "Stwórz nową grupę", + "no_groups_found": "Brak grup do wyświetlenia", + "cover-instructions": "Przeciągnij i upuść zdjęcie, ustaw w odpowiedniej pozycji i kliknij Zapisz", + "cover-change": "Zmień", + "cover-save": "Zapisz", + "cover-saving": "Zapisuję", "details.title": "Szczegóły grupy", "details.members": "Lista członków", - "details.pending": "Pending Members", + "details.pending": "Członkowie oczekujący", "details.has_no_posts": "Członkowie tej grupy nie napisali żadnych postów.", "details.latest_posts": "Ostatnie posty", - "details.private": "Private Group", - "details.public": "Public Group", - "details.grant": "Grant/Rescind Ownership", - "details.kick": "Kick", - "details.owner_options": "Group Administration", - "event.updated": "Group details have been updated", - "event.deleted": "The group \"%1\" has been deleted" + "details.private": "Grupa prywatna", + "details.public": "Grupa publiczna", + "details.grant": "Nadaj/Cofnij prawa Właściciela", + "details.kick": "Wykop", + "details.owner_options": "Administracja grupy", + "event.updated": "Dane grupy zostały zaktualizowane", + "event.deleted": "Grupa \"%1\" została skasowana" } \ No newline at end of file diff --git a/public/language/pl/pages.json b/public/language/pl/pages.json index 4aad8f6c6d..7f71ba388a 100644 --- a/public/language/pl/pages.json +++ b/public/language/pl/pages.json @@ -11,7 +11,7 @@ "user.followers": "Obserwujący %1", "user.posts": "Posty napisane przez %1", "user.topics": "Wątki stworzone przez %1", - "user.groups": "%1's Groups", + "user.groups": "Grupy %1", "user.favourites": "Ulubione posty %1", "user.settings": "Ustawienia użytkownika", "maintenance.text": "Obecnie trwają prace konserwacyjne nad %1. Proszę wrócić później.", diff --git a/public/language/pl/recent.json b/public/language/pl/recent.json index 382c9d8095..3a5ed0fa33 100644 --- a/public/language/pl/recent.json +++ b/public/language/pl/recent.json @@ -6,13 +6,13 @@ "year": "Rok", "alltime": "Od początku", "no_recent_topics": "Brak ostatnich wątków.", - "there-is-a-new-topic": "There is a new topic.", - "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", - "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", - "there-are-new-topics": "There are %1 new topics.", - "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.", - "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", - "there-is-a-new-post": "There is a new post.", - "there-are-new-posts": "There are %1 new posts.", - "click-here-to-reload": "Click here to reload." + "there-is-a-new-topic": "Masz nowy wątek.", + "there-is-a-new-topic-and-a-new-post": "Masz nowy wątek i nowy post.", + "there-is-a-new-topic-and-new-posts": "Masz nowy wątek i %1 nowych postów.", + "there-are-new-topics": "Masz %1 nowych wątków.", + "there-are-new-topics-and-a-new-post": "Masz %1 nowych wątków i nowy post.", + "there-are-new-topics-and-new-posts": "Masz %1 nowych wątków i %2 nowych postów.", + "there-is-a-new-post": "Masz nowy post.", + "there-are-new-posts": "Masz %1 nowych postów.", + "click-here-to-reload": "Kliknij tutaj, aby przeładować." } \ No newline at end of file diff --git a/public/language/pl/reset_password.json b/public/language/pl/reset_password.json index 99b7518a97..57b58f35e3 100644 --- a/public/language/pl/reset_password.json +++ b/public/language/pl/reset_password.json @@ -11,6 +11,6 @@ "enter_email_address": "Wpisz swój adres e-mail", "password_reset_sent": "Instrukcje zostały wysłane", "invalid_email": "Niepoprawny adres e-mail.", - "password_too_short": "The password entered is too short, please pick a different password.", - "passwords_do_not_match": "The two passwords you've entered do not match." + "password_too_short": "Wprowadzone hasło jest zbyt krótkie, proszę wybierz inne hasło.", + "passwords_do_not_match": "Wprowadzone hasła nie pasują do siebie" } \ No newline at end of file diff --git a/public/language/pl/search.json b/public/language/pl/search.json index c8775e8c66..6f2afda91d 100644 --- a/public/language/pl/search.json +++ b/public/language/pl/search.json @@ -1,35 +1,35 @@ { "results_matching": "%1 wyników pasujących do \"%2\", (%3 sekund)", - "no-matches": "No matches found", - "in": "In", - "by": "By", - "titles": "Titles", - "titles-posts": "Titles and Posts", - "posted-by": "Posted by", - "in-categories": "In Categories", - "search-child-categories": "Search child categories", - "reply-count": "Reply Count", - "at-least": "At least", - "at-most": "At most", - "post-time": "Post time", - "newer-than": "Newer than", - "older-than": "Older than", - "any-date": "Any date", - "yesterday": "Yesterday", - "one-week": "One week", - "two-weeks": "Two weeks", - "one-month": "One month", - "three-months": "Three months", - "six-months": "Six months", - "one-year": "One year", - "sort-by": "Sort by", - "last-reply-time": "Last reply time", - "topic-title": "Topic title", - "number-of-replies": "Number of replies", - "number-of-views": "Number of views", - "topic-start-date": "Topic start date", - "username": "Username", - "category": "Category", - "descending": "In descending order", - "ascending": "In ascending order" + "no-matches": "Nie znaleziono pasujących wyników", + "in": "W", + "by": "Przez", + "titles": "Tytuły", + "titles-posts": "Tytuły i posty", + "posted-by": "Napisane przez", + "in-categories": "W kategoriach", + "search-child-categories": "Przeszukaj podkategorie", + "reply-count": "Ilość odpowiedzi", + "at-least": "Przynajmniej", + "at-most": "Co najwyżej", + "post-time": "Napisano", + "newer-than": "Nowsze niż", + "older-than": "Starsze niż", + "any-date": "Kiedykolwiek", + "yesterday": "Wczoraj", + "one-week": "Jeden tydzień", + "two-weeks": "Dwa tygodnie", + "one-month": "Jeden miesiąc", + "three-months": "Trzy miesiące", + "six-months": "Sześć miesięcy", + "one-year": "Jeden rok", + "sort-by": "Sortuj po", + "last-reply-time": "Odpowiedziano ostatnio", + "topic-title": "Tytuł wątku", + "number-of-replies": "Ilość odpowiedzi", + "number-of-views": "Ilość wyświetleń", + "topic-start-date": "Data utworzenia wątku", + "username": "Nazwa użytkownika", + "category": "Kategoria", + "descending": "W kolejności malejącej", + "ascending": "W kolejności rosnącej" } \ No newline at end of file diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index e5d3a154ae..e9941d4abf 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -12,6 +12,7 @@ "notify_me": "Powiadamiaj mnie o nowych odpowiedziach w tym wątku", "quote": "Cytuj", "reply": "Odpowiedz", + "guest-login-reply": "Log in to reply", "edit": "Edytuj", "delete": "Usuń", "purge": "Wymaż", @@ -74,7 +75,7 @@ "fork_no_pids": "Nie zaznaczyłeś żadnych postów!", "fork_success": "Udało się skopiować wątek. Kliknij tutaj, aby do niego przejść.", "composer.title_placeholder": "Wpisz tytuł wątku tutaj", - "composer.handle_placeholder": "Name", + "composer.handle_placeholder": "Nazwa", "composer.discard": "Odrzuć", "composer.submit": "Wyślij", "composer.replying_to": "Odpowiadanie na %1", @@ -94,5 +95,5 @@ "oldest_to_newest": "Najpierw najstarsze", "newest_to_oldest": "Najpierw najnowsze", "most_votes": "Najwięcej głosów", - "most_posts": "Most posts" + "most_posts": "Najwięcej postów" } \ No newline at end of file diff --git a/public/language/pl/user.json b/public/language/pl/user.json index b031c36d5a..e28e3f637f 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -2,8 +2,8 @@ "banned": "Zbanowany", "offline": "Offline", "username": "Nazwa użytkownika", - "joindate": "Join Date", - "postcount": "Post Count", + "joindate": "Data rejestracji", + "postcount": "Liczba postów", "email": "Adres e-mail", "confirm_email": "Potwierdź e-mail", "delete_account": "Skasuj konto", @@ -18,7 +18,7 @@ "profile_views": "wyświetleń", "reputation": "reputacji", "favourites": "Ulubione", - "watched": "Watched", + "watched": "Obserwowane", "followers": "Obserwujących", "following": "Obserwowanych", "signature": "Sygnatura", @@ -59,12 +59,12 @@ "digest_weekly": "Co tydzień", "digest_monthly": "Co miesiąc", "send_chat_notifications": "Wyślij e-maila, jeśli dostanę nową wiadomość, a nie jestem on-line", - "send_post_notifications": "Send an email when replies are made to topics I am subscribed to", + "send_post_notifications": "Wyślij e-maila, kiedy wątki, które subskrybuję otrzymają odpowiedź", "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.", "has_no_topics": "Ten użytkownik nie napisał jeszcze żadnego wątku.", - "has_no_watched_topics": "This user didn't watch any topics yet.", + "has_no_watched_topics": "Ten użytkownik nie obserwował jeszcze żadnego wątku.", "email_hidden": "Adres e-mail ukryty", "hidden": "ukryty", "paginate_description": "Użyj klasycznego trybu paginacji zamiast nieskończonego przewijania.", diff --git a/public/language/pl/users.json b/public/language/pl/users.json index 2df9600a30..b41b99309a 100644 --- a/public/language/pl/users.json +++ b/public/language/pl/users.json @@ -5,8 +5,8 @@ "search": "Szukaj", "enter_username": "Wpisz nazwę użytkownika", "load_more": "Więcej", - "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", - "filter-by": "Filter By", - "online-only": "Online only", - "picture-only": "Picture only" + "users-found-search-took": "Znaleziono %1 użytkownik(ów). Szukanie zajęło %2 sek.", + "filter-by": "Filtruj", + "online-only": "Tylko dostępny", + "picture-only": "Tylko ze zdjęciem" } \ No newline at end of file diff --git a/public/language/pt_BR/category.json b/public/language/pt_BR/category.json index 42b2b4017f..a781984e16 100644 --- a/public/language/pt_BR/category.json +++ b/public/language/pt_BR/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Novo Tópico", + "guest-login-post": "Log in to post", "no_topics": "Não tem nenhum tópico nesta categoria.
Por que você não tenta postar o algum?", "browsing": "navegando", "no_replies": "Ninguém respondeu", diff --git a/public/language/pt_BR/email.json b/public/language/pt_BR/email.json index 250bf39788..cca995c0a5 100644 --- a/public/language/pt_BR/email.json +++ b/public/language/pt_BR/email.json @@ -9,9 +9,9 @@ "reset.text1": "Nós recebemos um pedido para reconfigurar sua senha, possivelmente porque você a esqueceu. Se este não é o caso, por favor ignore este email.", "reset.text2": "Para continuar com a reconfiguração de senha, por favor clique no seguinte link:", "reset.cta": "Clique aqui para reconfigurar sua senha", - "reset.notify.subject": "Password successfully changed", - "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", - "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "reset.notify.subject": "Senha alterada com sucesso", + "reset.notify.text1": "Nós estamos notificando você que em %1, sua senha foi alterada com sucesso.", + "reset.notify.text2": "Se você não autorizou isso, por favor notifique um administrador imediatamente.", "digest.notifications": "Você tem notificações não lidas de %1:", "digest.latest_topics": "Últimos tópicos de %1", "digest.cta": "Clique aqui para visitar %1", diff --git a/public/language/pt_BR/error.json b/public/language/pt_BR/error.json index 51d9f13502..e0ad9b1e67 100644 --- a/public/language/pt_BR/error.json +++ b/public/language/pt_BR/error.json @@ -35,7 +35,7 @@ "topic-locked": "Tópico Trancado", "still-uploading": "Aguarde a conclusão dos uploads.", "content-too-short": "Por favor digite um post mais longo. Posts devem conter no mínimo %1 caracteres.", - "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 characters.", + "content-too-long": "Por favor entre com um post mais curto. Posts não podem ser maiores do que %1 caracteres.", "title-too-short": "Por favor digite um título mais longo. Títulos devem conter no mínimo %1 caracteres.", "title-too-long": "Por favor entre com um título mais curto; Títulos não podem ser maiores que %1 caracteres.", "too-many-posts": "Você pode postar apenas uma vez a cada %1 segundos - por favor aguarde antes de postar novamente", diff --git a/public/language/pt_BR/groups.json b/public/language/pt_BR/groups.json index 4095f4c3ce..f3a19ea8ef 100644 --- a/public/language/pt_BR/groups.json +++ b/public/language/pt_BR/groups.json @@ -15,8 +15,8 @@ "details.latest_posts": "Últimos Posts", "details.private": "Grupo Privado", "details.public": "Grupo Público.", - "details.grant": "Grant/Rescind Ownership", - "details.kick": "Kick", + "details.grant": "Conceder/Retomar a Posse", + "details.kick": "Chutar", "details.owner_options": "Administração do Grupo", "event.updated": "Os detalhes do grupo foram atualizados", "event.deleted": "O grupo \"%1\" foi deletado" diff --git a/public/language/pt_BR/pages.json b/public/language/pt_BR/pages.json index ae42c35f0f..415f50c0fd 100644 --- a/public/language/pt_BR/pages.json +++ b/public/language/pt_BR/pages.json @@ -11,7 +11,7 @@ "user.followers": "Pessoas que Seguem %1", "user.posts": "Posts feitos por %1", "user.topics": "Tópicos criados por %1", - "user.groups": "%1's Groups", + "user.groups": "%1's Grupos", "user.favourites": "Posts Favoritos de %1", "user.settings": "Configurações de Usuário", "maintenance.text": "%1 está atualmente sob manutenção. Por favor retorne em outro momento.", diff --git a/public/language/pt_BR/reset_password.json b/public/language/pt_BR/reset_password.json index bf96416678..3aa6f341b4 100644 --- a/public/language/pt_BR/reset_password.json +++ b/public/language/pt_BR/reset_password.json @@ -11,6 +11,6 @@ "enter_email_address": "Digite seu Email", "password_reset_sent": "Reconfiguração de Senha Enviada", "invalid_email": "Email Inválido / Email não existe!", - "password_too_short": "The password entered is too short, please pick a different password.", - "passwords_do_not_match": "The two passwords you've entered do not match." + "password_too_short": "A senha entrada é muito curta, por favor escolha uma senha diferente.", + "passwords_do_not_match": "As duas senhas que você digitou não combinam." } \ No newline at end of file diff --git a/public/language/pt_BR/search.json b/public/language/pt_BR/search.json index 9f10c0970b..f1b47b737e 100644 --- a/public/language/pt_BR/search.json +++ b/public/language/pt_BR/search.json @@ -3,33 +3,33 @@ "no-matches": "Nenhum resultado encontrado", "in": "Em", "by": "Por", - "titles": "Titles", - "titles-posts": "Titles and Posts", + "titles": "Títulos", + "titles-posts": "Títulos e Posts", "posted-by": "Postado por", - "in-categories": "In Categories", - "search-child-categories": "Search child categories", - "reply-count": "Reply Count", - "at-least": "At least", - "at-most": "At most", - "post-time": "Post time", - "newer-than": "Newer than", - "older-than": "Older than", - "any-date": "Any date", - "yesterday": "Yesterday", - "one-week": "One week", - "two-weeks": "Two weeks", - "one-month": "One month", - "three-months": "Three months", - "six-months": "Six months", - "one-year": "One year", - "sort-by": "Sort by", - "last-reply-time": "Last reply time", - "topic-title": "Topic title", - "number-of-replies": "Number of replies", - "number-of-views": "Number of views", - "topic-start-date": "Topic start date", - "username": "Username", - "category": "Category", - "descending": "In descending order", - "ascending": "In ascending order" + "in-categories": "Nas Categorias", + "search-child-categories": "Buscar categorias filhas", + "reply-count": "Contagem de Respostas", + "at-least": "No mínimo", + "at-most": "No máximo", + "post-time": "Hora da Postagem", + "newer-than": "Mais novo que", + "older-than": "Mais velho que", + "any-date": "Qualquer data", + "yesterday": "Ontem", + "one-week": "Uma semana", + "two-weeks": "Duas semanas", + "one-month": "Um mês", + "three-months": "Três meses", + "six-months": "Seis meses", + "one-year": "Um ano", + "sort-by": "Ordenar por", + "last-reply-time": "Tempo da última resposta", + "topic-title": "Título do tópico", + "number-of-replies": "Número de respostas", + "number-of-views": "Número de visualizações", + "topic-start-date": "Data do início do tópico", + "username": "Nome de usuário", + "category": "Categoria", + "descending": "Em ordem descendente", + "ascending": "Em ordem ascendente" } \ No newline at end of file diff --git a/public/language/pt_BR/topic.json b/public/language/pt_BR/topic.json index df49f8cb20..a99468a79f 100644 --- a/public/language/pt_BR/topic.json +++ b/public/language/pt_BR/topic.json @@ -12,6 +12,7 @@ "notify_me": "Seja notificado de novas respostas nesse tópico", "quote": "Citar", "reply": "Responder", + "guest-login-reply": "Log in to reply", "edit": "Editar", "delete": "Deletar", "purge": "Expurgar", diff --git a/public/language/pt_BR/users.json b/public/language/pt_BR/users.json index 32edc568f5..cc49b0fe57 100644 --- a/public/language/pt_BR/users.json +++ b/public/language/pt_BR/users.json @@ -5,7 +5,7 @@ "search": "Procurar", "enter_username": "Digite um nome de usuário para procurar", "load_more": "Carregar Mais", - "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", + "users-found-search-took": "%1 usuário(s) encontrado(s)! A busca levou %2 segundos.", "filter-by": "Filtrar Por", "online-only": "Apenas Online", "picture-only": "Apenas foto" diff --git a/public/language/ro/category.json b/public/language/ro/category.json index 7ff3d49fe6..733f5de0d7 100644 --- a/public/language/ro/category.json +++ b/public/language/ro/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Subiect Nou", + "guest-login-post": "Log in to post", "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", diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json index 49803e1823..590dfed6d5 100644 --- a/public/language/ro/topic.json +++ b/public/language/ro/topic.json @@ -12,6 +12,7 @@ "notify_me": "Notică-mă de noi răspunsuri în acest subiect", "quote": "Citează", "reply": "Răspunde", + "guest-login-reply": "Log in to reply", "edit": "Editează", "delete": "Șterge", "purge": "Curăță", diff --git a/public/language/ru/category.json b/public/language/ru/category.json index f80eccd5ba..8a97af5a0d 100644 --- a/public/language/ru/category.json +++ b/public/language/ru/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Создать тему", + "guest-login-post": "Log in to post", "no_topics": "В этой категории еще нет тем.
Почему бы вам не создать первую?", "browsing": "просматривают", "no_replies": "Нет ответов", diff --git a/public/language/ru/email.json b/public/language/ru/email.json index eed54608d0..4353d9101e 100644 --- a/public/language/ru/email.json +++ b/public/language/ru/email.json @@ -9,9 +9,9 @@ "reset.text1": "Мы получили запрос на сброс Вашего пароля. Если Вы не подавали запрос, пожалуйста, проигнорируйте это сообщение.", "reset.text2": "Для продолжения процедуры изменения пароля, пожалуйста, перейдите по ссылке:", "reset.cta": "Кликните здесь для изменения пароля", - "reset.notify.subject": "Password successfully changed", - "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", - "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "reset.notify.subject": "Пароль был успешно изменен", + "reset.notify.text1": "Мы уведомляем вас о том, что %1 ваш пароль был успешно изменен. ", + "reset.notify.text2": "Если вы не совершали этого действия, пожалуйста, незамедлительно свяжитесь с администратором.", "digest.notifications": "У Вас непрочитанные уведомления от %1:", "digest.latest_topics": "Последние темы %1", "digest.cta": "Кликните здесь для просмотра %1", @@ -20,8 +20,8 @@ "notif.chat.subject": "Новое сообщение от %1", "notif.chat.cta": "Нажмите для продолжения диалога", "notif.chat.unsub.info": "Вы получили это уведомление в соответствии с настройками подписок.", - "notif.post.cta": "Click here to read the full topic", - "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.cta": "Кликните для просмотра всей темы.", + "notif.post.unsub.info": "Вы получили это уведомление согласно вашим настройкам подписки.", "test.text1": "Это тестовое сообщение для проверки почтового сервиса NodeBB.", "unsub.cta": "Изменить настройки", "closing": "Спасибо!" diff --git a/public/language/ru/error.json b/public/language/ru/error.json index 5683d863d1..25309f37d4 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -1,17 +1,17 @@ { - "invalid-data": "Неверные Данные", + "invalid-data": "Неверные данные", "not-logged-in": "Вы не вошли в свой аккаунт.", "account-locked": "Ваш аккаунт временно заблокирован", "search-requires-login": "Поиск доступен зарегистрированным пользователям! Пожалуйста, войдите или зарегистрируйтесь!", - "invalid-cid": "Неверный ID Категории", - "invalid-tid": "Неверный ID Темы", - "invalid-pid": "Неверный ID Поста", - "invalid-uid": "Неверный ID Пользователя", - "invalid-username": "Неверное Имя пользователя", + "invalid-cid": "Неверный ID категории", + "invalid-tid": "Неверный ID темы", + "invalid-pid": "Неверный ID поста", + "invalid-uid": "Неверный ID пользователя", + "invalid-username": "Неверное имя пользователя", "invalid-email": "Неверный Email", - "invalid-title": "Неверный заголовок!", - "invalid-user-data": "Неверные Пользовательские Данные", - "invalid-password": "Неверный Пароль", + "invalid-title": "Неверный заголовок", + "invalid-user-data": "Неверные пользовательские данные", + "invalid-password": "Неверный пароль", "invalid-username-or-password": "Пожалуйста, укажите и имя пользователя и пароль", "invalid-search-term": "Неверный поисковой запрос", "invalid-pagination-value": "Неверное значение пагинации", @@ -35,7 +35,7 @@ "topic-locked": "Тема закрыта", "still-uploading": "Пожалуйста, подождите завершения загрузки.", "content-too-short": "Пост должен содержать минимум %1 симв.", - "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 characters.", + "content-too-long": "Размер поста не должен превышать %1 символов. Пожалуйста, сделайте его короче.", "title-too-short": "Заголовок должен содержать минимум %1 симв.", "title-too-long": "Заголовок не может быть длиннее %1 символов.", "too-many-posts": "Вы можете делать пост один раз в %1 сек.", @@ -45,7 +45,7 @@ "already-favourited": "Вы уже добавили этот пост в избранное", "already-unfavourited": "Вы уже удалили этот пост из избранного", "cant-ban-other-admins": "Вы не можете забанить других администраторов!", - "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-type": "Неверный формат изображения. Поддерживаемые форматы: %1", "invalid-image-extension": "Недопустимое расширение файла", "group-name-too-short": "Название группы слишком короткое", "group-already-exists": "Группа уже существует", @@ -70,5 +70,5 @@ "not-enough-reputation-to-flag": "У Вас недостаточно репутации, чтобы пометить этот пост.", "reload-failed": "NodeBB обнаружил проблему при перезагрузке: \"%1\". NodeBB продолжит работать с существующими ресурсами клиента, но Вы должны отменить то, что сделали перед перезагрузкой.", "registration-error": "Ошибка при регистрации", - "parse-error": "Something went wrong while parsing server response" + "parse-error": "Похоже, что-то пошло не так в процессе обработки ответа сервера." } \ No newline at end of file diff --git a/public/language/ru/global.json b/public/language/ru/global.json index 1983e9f1ab..700ed17127 100644 --- a/public/language/ru/global.json +++ b/public/language/ru/global.json @@ -3,10 +3,10 @@ "search": "Поиск", "buttons.close": "Закрыть", "403.title": "Доступ запрещен", - "403.message": "You seem to have stumbled upon a page that you do not have access to.", + "403.message": "Вы пытаетесь перейти на страницу, к которой у вас нет прав доступа.", "403.login": "Возможно Вам следует войти под своим аккаунтом?", "404.title": "Страница не найдена", - "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "404.message": "Вы пытаетесь перейти на страницу, которой не существует. Вам стоит вернутся на главную страницу.", "500.title": "Внутренняя ошибка.", "500.message": "Упс! Похоже, что-то пошло не так!", "register": "Зарегистрироваться", @@ -75,7 +75,7 @@ "updated.title": "Форум обновлен", "updated.message": "Форум был обновлен до последней версии. Нажмите здесь, чтобы обновить страницу.", "privacy": "Безопасность", - "follow": "Follow", - "unfollow": "Unfollow", + "follow": "Подписаться", + "unfollow": "Отписаться", "delete_all": "Удалить все" } \ No newline at end of file diff --git a/public/language/ru/groups.json b/public/language/ru/groups.json index 4b86076056..544998a50e 100644 --- a/public/language/ru/groups.json +++ b/public/language/ru/groups.json @@ -1,11 +1,11 @@ { "groups": "Группы", "view_group": "Просмотр группы", - "owner": "Владелец группы", + "owner": "Администратор группы", "new_group": "Создать группу", - "no_groups_found": "There are no groups to see", + "no_groups_found": "Нет групп для отображения", "cover-instructions": "Перетяните сюда изображение, переместите на нужную позицию и нажмите Сохранить", - "cover-change": "Change", + "cover-change": "Изменить", "cover-save": "Сохранить", "cover-saving": "Сохраняем", "details.title": "Информация о группе", @@ -15,8 +15,8 @@ "details.latest_posts": "Последние записи", "details.private": "Приватная группа", "details.public": "Открытая группа", - "details.grant": "Grant/Rescind Ownership", - "details.kick": "Kick", + "details.grant": "Выдать/забрать администратора", + "details.kick": "Исключить", "details.owner_options": "Настройки группы", "event.updated": "Настройки группы обновлены", "event.deleted": "Группа \"%1\" удалена" diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json index 73a6dfb38b..4cc64159b5 100644 --- a/public/language/ru/notifications.json +++ b/public/language/ru/notifications.json @@ -19,7 +19,7 @@ "user_posted_topic": "%1 открыл новую тему: %2", "user_mentioned_you_in": "%1 упомянул Вас в %2", "user_started_following_you": "%1 подписался на Вас.", - "email-confirmed": "Email Подтвержден", + "email-confirmed": "Email подтвержден", "email-confirmed-message": "Спасибо за подтверждение Вашего Email-адреса. Ваш аккаунт активирован.", "email-confirm-error": "Произошла ошибка...", "email-confirm-error-message": "Ошибка проверки Email-адреса. Возможно, код неверен, либо у него истек срок действия.", diff --git a/public/language/ru/pages.json b/public/language/ru/pages.json index f8a014193b..9ad5cae0c2 100644 --- a/public/language/ru/pages.json +++ b/public/language/ru/pages.json @@ -11,7 +11,7 @@ "user.followers": "Читают %1", "user.posts": "Пост написан %1", "user.topics": "Темы созданы %1", - "user.groups": "%1's Groups", + "user.groups": "Группы %1", "user.favourites": "Избранные сообщения %1", "user.settings": "Настройки", "maintenance.text": "%1 в настоящее время на обслуживании. Пожалуйста, возвращайтесь позже.", diff --git a/public/language/ru/register.json b/public/language/ru/register.json index 419a808877..eae56a5403 100644 --- a/public/language/ru/register.json +++ b/public/language/ru/register.json @@ -3,16 +3,16 @@ "help.email": "По умолчанию, ваш email будет скрыт.", "help.username_restrictions": "Уникальное Имя между %1 и %2 символов. Другие пользователи смогут упоминать вас по @Имени.", "help.minimum_password_length": "Длина вашего пароля должна быть минимум %1 символов.", - "email_address": "Email Адрес", + "email_address": "Email адрес", "email_address_placeholder": "Введите Email адрес", "username": "Имя пользователя", - "username_placeholder": "Введите Имя пользователя", + "username_placeholder": "Введите имя пользователя", "password": "Пароль", - "password_placeholder": "Введите Пароль", - "confirm_password": "Подтвердите Пароль", - "confirm_password_placeholder": "Подтвердите Пароль", + "password_placeholder": "Введите пароль", + "confirm_password": "Подтвердите пароль", + "confirm_password_placeholder": "Подтвердите пароль", "register_now_button": "Зарегистрироваться", - "alternative_registration": "Альтернативная Регистрация", + "alternative_registration": "Альтернативная регистрация", "terms_of_use": "Условия использования", "agree_to_terms_of_use": "Я согласен с условиями" } \ No newline at end of file diff --git a/public/language/ru/reset_password.json b/public/language/ru/reset_password.json index c9b523cf2e..a2226d5e60 100644 --- a/public/language/ru/reset_password.json +++ b/public/language/ru/reset_password.json @@ -1,16 +1,16 @@ { - "reset_password": "Восстановить Пароль", - "update_password": "Изменить Пароль", - "password_changed.title": "Пароль Изменен", + "reset_password": "Восстановить пароль", + "update_password": "Изменить пароль", + "password_changed.title": "Пароль изменен", "password_changed.message": "

Пароль успешно восстановлен, пожалуйста войдите еще раз.", "wrong_reset_code.title": "Неверный код восстановления", "wrong_reset_code.message": "Неправильный код восстановления пароля. Попробуйте еще раз, или запросите новый код восстановления.", - "new_password": "Новый Пароль", - "repeat_password": "Подтвердите Пароль", + "new_password": "Новый пароль", + "repeat_password": "Подтвердите пароль", "enter_email": "Пожалуйста введите ваш email адрес и мы отправим Вам письмо с инструкцией восстановления пароля.", "enter_email_address": "Введите Email адрес", - "password_reset_sent": "Пароль Отправлен", + "password_reset_sent": "Пароль отправлен", "invalid_email": "Неверный Email / Email не существует!", - "password_too_short": "The password entered is too short, please pick a different password.", - "passwords_do_not_match": "The two passwords you've entered do not match." + "password_too_short": "Введенный пароль слишком короткий, пожалуйста, введите более длинный пароль.", + "passwords_do_not_match": "Введенные пароли не совпадают." } \ No newline at end of file diff --git a/public/language/ru/search.json b/public/language/ru/search.json index f6ecb8d611..943b857c9b 100644 --- a/public/language/ru/search.json +++ b/public/language/ru/search.json @@ -1,35 +1,35 @@ { "results_matching": "%1 результатов по фразе \"%2\", (%3 секунды) ", "no-matches": "Совпадений не найдено", - "in": "In", - "by": "By", - "titles": "Titles", - "titles-posts": "Titles and Posts", - "posted-by": "Posted by", - "in-categories": "In Categories", - "search-child-categories": "Search child categories", - "reply-count": "Reply Count", - "at-least": "At least", - "at-most": "At most", - "post-time": "Post time", - "newer-than": "Newer than", - "older-than": "Older than", - "any-date": "Any date", - "yesterday": "Yesterday", - "one-week": "One week", - "two-weeks": "Two weeks", - "one-month": "One month", - "three-months": "Three months", - "six-months": "Six months", - "one-year": "One year", - "sort-by": "Sort by", - "last-reply-time": "Last reply time", - "topic-title": "Topic title", - "number-of-replies": "Number of replies", - "number-of-views": "Number of views", - "topic-start-date": "Topic start date", - "username": "Username", - "category": "Category", - "descending": "In descending order", - "ascending": "In ascending order" + "in": "В", + "by": "От", + "titles": "Названия", + "titles-posts": "Названия и сообщения", + "posted-by": "Написано ", + "in-categories": "В разделах", + "search-child-categories": "Искать во вложенных разделах", + "reply-count": "Количество ответов", + "at-least": "Минимум", + "at-most": "Максимум", + "post-time": "Время публикации", + "newer-than": "Ранее чем", + "older-than": "Позже чем", + "any-date": "Любая дата", + "yesterday": "Вчера", + "one-week": "Одна неделя", + "two-weeks": "Две недели", + "one-month": "Один месяц", + "three-months": "Три месяца", + "six-months": "Шесть месяцев", + "one-year": "Год", + "sort-by": "Сортировать по", + "last-reply-time": "Время последнего ответа", + "topic-title": "Название темы", + "number-of-replies": "Количество ответов", + "number-of-views": "Количество просмотров", + "topic-start-date": "Время создания темы", + "username": "Имя пользователя", + "category": "Категория", + "descending": "В порядке возрастания", + "ascending": "В порядке убывания" } \ No newline at end of file diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index 644b66d9ae..635e31a765 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -7,11 +7,12 @@ "post_is_deleted": "Этот пост удален!", "profile": "Профиль", "posted_by": "Создано %1", - "posted_by_guest": "Опубликовано Гостем", + "posted_by_guest": "Опубликовано гостем", "chat": "Чат", "notify_me": "Сообщать мне об ответах в этой теме", "quote": "Цитировать", "reply": "Ответить", + "guest-login-reply": "Войдите чтобы оставить сообщение", "edit": "Редактировать", "delete": "Удалить", "purge": "Очистить", @@ -39,60 +40,60 @@ "share_this_post": "Поделиться этим Постом", "thread_tools.title": "Опции темы", "thread_tools.markAsUnreadForAll": "Отметить как непрочитанные", - "thread_tools.pin": "Прикрепить Тему", - "thread_tools.unpin": "Открепить Тему", - "thread_tools.lock": "Закрыть Тему", - "thread_tools.unlock": "Открыть Тему", - "thread_tools.move": "Переместить Тему", - "thread_tools.move_all": "Переместить Все", - "thread_tools.fork": "Ответвить Тему", - "thread_tools.delete": "Удалить Тему", + "thread_tools.pin": "Прикрепить тему", + "thread_tools.unpin": "Открепить тему", + "thread_tools.lock": "Закрыть тему", + "thread_tools.unlock": "Открыть тему", + "thread_tools.move": "Переместить тему", + "thread_tools.move_all": "Переместить все", + "thread_tools.fork": "Ответвить тему", + "thread_tools.delete": "Удалить тему", "thread_tools.delete_confirm": "Вы уверены, что хотите удалить тему?", - "thread_tools.restore": "Восстановить Тему", + "thread_tools.restore": "Восстановить тему", "thread_tools.restore_confirm": "Вы уверены, что хотите восстановить тему?", - "thread_tools.purge": "Очистить Тему", + "thread_tools.purge": "Очистить тему", "thread_tools.purge_confirm": "Вы уверены, что хотите очистить эту тему?", "topic_move_success": "Эта тема успешно перемещена в %1", "post_delete_confirm": "Вы уверены, что хотите удалить этот пост?", "post_restore_confirm": "Вы уверены, что хотите восстановить этот пост?", "post_purge_confirm": "Вы уверены, что хотите очистить эту запись?", - "load_categories": "Загружаем Категории", - "disabled_categories_note": "Отключенные категории затемненны", + "load_categories": "Загружаем категории", + "disabled_categories_note": "Отключенные категории затемнены", "confirm_move": "Перенести", "confirm_fork": "Ответвление", "favourite": "Избранное", "favourites": "Избранные", - "favourites.has_no_favourites": "У вас нет избранного, добавьте несколько сообщений в избранное чтобы увидеть их здесь!", + "favourites.has_no_favourites": "У вас нет избранного, добавьте несколько сообщений в избранное, чтобы увидеть их здесь", "loading_more_posts": "Загружаем еще сообщения", "move_topic": "Перенести тему", - "move_topics": "Больше Тем", + "move_topics": "Перенести темы", "move_post": "Перенести сообщение", - "post_moved": "Пост перемещен!", - "fork_topic": "Ответвить Тему", + "post_moved": "Пост перенесен", + "fork_topic": "Ответвить тему", "topic_will_be_moved_to": "Эта тема будет перенесена в категорию", "fork_topic_instruction": "Отметьте сообщения для ответвления", "fork_no_pids": "Сообщения не отмечены!", "fork_success": "Готово! Нажмите для перехода в отделённую тему.", "composer.title_placeholder": "Введите название темы...", - "composer.handle_placeholder": "Name", + "composer.handle_placeholder": "Название", "composer.discard": "Отменить", "composer.submit": "Подтвердить", "composer.replying_to": "Ответ %1", - "composer.new_topic": "Создать Тему", + "composer.new_topic": "Создать тему", "composer.uploading": "загрузка...", "composer.thumb_url_label": "Вставьте URL картинки с иконкой темы.", "composer.thumb_title": "Добавить иконку к этой теме", "composer.thumb_url_placeholder": "http://example.com/thumb.png", "composer.thumb_file_label": "Или загрузите файл", "composer.thumb_remove": "Очистить поля", - "composer.drag_and_drop_images": "Перетащите Изображения Сюда", + "composer.drag_and_drop_images": "Перетащите изображения сюда", "more_users_and_guests": "еще %1 пользователя(ей) и %2 гостя(ей)", "more_users": "еще %1 пользователя(ей)", "more_guests": "еще %1 гостя(ей)", "users_and_others": "%1 и %2 других", "sort_by": "Сортировать", - "oldest_to_newest": "От Старых к Новым", - "newest_to_oldest": "От Новых к Старым", + "oldest_to_newest": "От старых к новым", + "newest_to_oldest": "От новых к старым", "most_votes": "По голосам", - "most_posts": "Most posts" + "most_posts": "По количеству ответов" } \ No newline at end of file diff --git a/public/language/ru/unread.json b/public/language/ru/unread.json index 325a793d59..0bdf6c4933 100644 --- a/public/language/ru/unread.json +++ b/public/language/ru/unread.json @@ -2,7 +2,7 @@ "title": "Непрочитанные темы", "no_unread_topics": "Нет непрочитанных тем.", "load_more": "Загрузить еще", - "mark_as_read": "Пометить Прочитанным", + "mark_as_read": "Пометить прочитанным", "selected": "Выбраны", "all": "Все", "topics_marked_as_read.success": "Темы помечены как прочитанные!" diff --git a/public/language/ru/user.json b/public/language/ru/user.json index ba337e227b..0b5a541199 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -6,7 +6,7 @@ "postcount": "Сообщений", "email": "Email", "confirm_email": "Подтвердить Email", - "delete_account": "Удалить Аккаунт", + "delete_account": "Удалить аккаунт", "delete_account_confirm": "Вы уверены, что хотите удалить аккаунт?
Это действие необратимо, Вы не сможете восстановить свои данные

Введите имя пользователя для подтверждения уничтожения аккаунта.", "fullname": "Полное имя", "website": "Сайт", @@ -18,7 +18,7 @@ "profile_views": "Просмотров профиля", "reputation": "Репутация", "favourites": "Избранное", - "watched": "Watched", + "watched": "Просмотров", "followers": "Читателей", "following": "Читаемых", "signature": "Подпись", @@ -30,18 +30,18 @@ "profile_update_success": "Профиль обновлен!", "change_picture": "Изменить фотографию", "edit": "Редактировать", - "uploaded_picture": "Загруженные Фотографии", + "uploaded_picture": "Загруженные фотографии", "upload_new_picture": "Загрузить новую фотографию", "upload_new_picture_from_url": "Загрузить новое изображение с адреса URL", - "current_password": "Текущий Пароль", - "change_password": "Изменить Пароль", - "change_password_error": "Неверный Пароль!", + "current_password": "Текущий пароль", + "change_password": "Изменить пароль", + "change_password_error": "Неверный пароль!", "change_password_error_wrong_current": "Текущий пароль неверен!", "change_password_error_length": "Пароль слишком короткий!", "change_password_error_match": "Пароли должны совпадать!", "change_password_error_privileges": "Вы не можете сменить этот пароль.", "change_password_success": "Ваш пароль изменен!", - "confirm_password": "Подтвердите Пароль", + "confirm_password": "Подтвердите пароль", "password": "Пароль", "username_taken_workaround": "Логин, который Вы запросили, уже занят. Мы его немного изменили. Теперь Ваш логин %1", "upload_picture": "Загрузить фотографию", @@ -52,20 +52,20 @@ "show_email": "Показывать мой Email", "show_fullname": "Показывать Полное Имя", "restrict_chats": "Разрешить чат только с теми, на кого я подписан", - "digest_label": "Подписаться на Дайджест", + "digest_label": "Подписаться на дайджест", "digest_description": "Подписаться на Email обновления этого форума (новые уведомления и топики) согласно расписанию", "digest_off": "Выключить", - "digest_daily": "За День", - "digest_weekly": "За Неделю", - "digest_monthly": "За Месяц", + "digest_daily": "За день", + "digest_weekly": "За неделю", + "digest_monthly": "За месяц", "send_chat_notifications": "Уведомлять на E-mail при поступлении нового сообщения чата, когда я оффлайн", "send_post_notifications": "Отправлять email, когда отвечают в темы, на которые я подписан(а)", "has_no_follower": "Этого пользователя никто не читает :(", "follows_no_one": "Этот пользователь никого не читает :(", "has_no_posts": "Это пользователь еще ничего не написал.", "has_no_topics": "Этот пользователь еще не создал ни одной темы", - "has_no_watched_topics": "This user didn't watch any topics yet.", - "email_hidden": "Email Скрыт", + "has_no_watched_topics": "Этот пользователь еще не просмотрел ни одной темы", + "email_hidden": "Email скрыт", "hidden": "скрыто", "paginate_description": "Использовать пагинацию тем и постов вместо бесконечной прокрутки", "topics_per_page": "Тем на Странице", @@ -73,7 +73,7 @@ "notification_sounds": "Звук при получении уведомления", "browsing": "Настройки просмотра", "open_links_in_new_tab": "Открывать ссылки, ведущие на другие сайты, в новой вкладке?", - "enable_topic_searching": "Включить Поиск по всей теме", + "enable_topic_searching": "Активировать поиск внутри тем", "topic_search_help": "Если включено, то стандартный \"Поиск на странице\" Вашего браузера будет осуществлять поиск по всей теме вместо одной её страницы.", "follow_topics_you_reply_to": "Следить за темами, в которых Вы отвечали.", "follow_topics_you_create": "Следить за темами, которые Вы создали." diff --git a/public/language/ru/users.json b/public/language/ru/users.json index e4253bcf62..ffc028bcf3 100644 --- a/public/language/ru/users.json +++ b/public/language/ru/users.json @@ -5,8 +5,8 @@ "search": "Поиск", "enter_username": "Введите имя пользователя для поиска", "load_more": "Загрузить еще", - "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", - "filter-by": "Filter By", - "online-only": "Online only", - "picture-only": "Picture only" + "users-found-search-took": "Найдено %1 пользователя(ей). Поиск занял %2 секунд. ", + "filter-by": "Сортировать по", + "online-only": "Только онлайн", + "picture-only": "Только с аватаром" } \ No newline at end of file diff --git a/public/language/sc/category.json b/public/language/sc/category.json index 35e7f7b522..306f0e2d04 100644 --- a/public/language/sc/category.json +++ b/public/language/sc/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Arresonada Noa", + "guest-login-post": "Log in to post", "no_topics": "Non bi sunt arresonadas in custa creze.
Pro ite non nde pones una?", "browsing": "navighende", "no_replies": "Perunu at rispostu", diff --git a/public/language/sc/topic.json b/public/language/sc/topic.json index e08b8e84ba..c0ce4121ff 100644 --- a/public/language/sc/topic.json +++ b/public/language/sc/topic.json @@ -12,6 +12,7 @@ "notify_me": "Imbia·mi notìficas pro is rispostas noas a custa arresonada", "quote": "Mèntova", "reply": "Risponde", + "guest-login-reply": "Log in to reply", "edit": "Acontza", "delete": "Contzella", "purge": "Purge", diff --git a/public/language/sk/category.json b/public/language/sk/category.json index 9ce9439a2e..62ef9414d6 100644 --- a/public/language/sk/category.json +++ b/public/language/sk/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nová téma", + "guest-login-post": "Log in to post", "no_topics": "V tejto kategórií zatiaľ nie sú žiadne príspevky.
Môžeš byť prvý!", "browsing": "prehliada", "no_replies": "Nikdo ešte neodpovedal", diff --git a/public/language/sk/topic.json b/public/language/sk/topic.json index 05ee94f28e..007cf0f22a 100644 --- a/public/language/sk/topic.json +++ b/public/language/sk/topic.json @@ -12,6 +12,7 @@ "notify_me": "Sledovať túto tému", "quote": "Citovať", "reply": "Odpovedať", + "guest-login-reply": "Log in to reply", "edit": "Upraviť", "delete": "Zmazať", "purge": "Purge", diff --git a/public/language/sv/category.json b/public/language/sv/category.json index 635f2c0ac4..41b49a95db 100644 --- a/public/language/sv/category.json +++ b/public/language/sv/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Nytt ämne", + "guest-login-post": "Log in to post", "no_topics": "Det finns inga ämnen i denna kategori.
Varför skapar inte du ett ämne?", "browsing": "läser", "no_replies": "Ingen har svarat", diff --git a/public/language/sv/topic.json b/public/language/sv/topic.json index 9d41afee2b..40b81f0ec4 100644 --- a/public/language/sv/topic.json +++ b/public/language/sv/topic.json @@ -12,6 +12,7 @@ "notify_me": "Få notiser om nya svar i detta ämne", "quote": "Citera", "reply": "Svara", + "guest-login-reply": "Log in to reply", "edit": "Ändra", "delete": "Ta bort", "purge": "Rensa", diff --git a/public/language/th/category.json b/public/language/th/category.json index 31246e2a05..387458a0dd 100644 --- a/public/language/th/category.json +++ b/public/language/th/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "กระทู้", + "guest-login-post": "Log in to post", "no_topics": "ยังไม่มีกระทู้ในหมวดนี้
โพสต์กระทู้แรก?", "browsing": "เรียกดู", "no_replies": "ยังไม่มีใครตอบ", diff --git a/public/language/th/topic.json b/public/language/th/topic.json index c7c9f4e944..652ebe5db5 100644 --- a/public/language/th/topic.json +++ b/public/language/th/topic.json @@ -12,6 +12,7 @@ "notify_me": "แจ้งเตือนเมื่อการตอบใหม่ในกระทู้นี้", "quote": "คำอ้างอิง", "reply": "ตอบ", + "guest-login-reply": "Log in to reply", "edit": "แก้ไข", "delete": "ลบ", "purge": "Purge", diff --git a/public/language/tr/category.json b/public/language/tr/category.json index ba1c4f34ef..34ac645f32 100644 --- a/public/language/tr/category.json +++ b/public/language/tr/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Yeni Başlık", + "guest-login-post": "Log in to post", "no_topics": " Bu kategoride hiç konu yok.
Yeni bir konu açmak istemez misiniz?", "browsing": "gözden geçiriliyor", "no_replies": "Kimse yanıtlamadı", diff --git a/public/language/tr/topic.json b/public/language/tr/topic.json index 6cd2424e0f..fa76fb4ee6 100644 --- a/public/language/tr/topic.json +++ b/public/language/tr/topic.json @@ -12,6 +12,7 @@ "notify_me": "Bu konudaki cevaplardan haberdar ol", "quote": "Alıntı", "reply": "Cevap", + "guest-login-reply": "Log in to reply", "edit": "Düzenle", "delete": "Sil", "purge": "Temizle", diff --git a/public/language/vi/category.json b/public/language/vi/category.json index 6d2efb5e1b..e536a54efb 100644 --- a/public/language/vi/category.json +++ b/public/language/vi/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "Chủ đề mới", + "guest-login-post": "Log in to post", "no_topics": "Không có bài viết trong danh mục này.
Hãy đăng một bài viết mới.", "browsing": "đang xem", "no_replies": "Chưa có bình luận nào", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 2c23893104..8c9f8cdfce 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -12,6 +12,7 @@ "notify_me": "Được thông báo khi có trả lời mới trong chủ đề này", "quote": "Trích dẫn", "reply": "Trả lời", + "guest-login-reply": "Log in to reply", "edit": "Chỉnh sửa", "delete": "Xóa", "purge": "Xóa hẳn", diff --git a/public/language/zh_CN/category.json b/public/language/zh_CN/category.json index 933e12e3e1..21ea5d5acf 100644 --- a/public/language/zh_CN/category.json +++ b/public/language/zh_CN/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "新主题", + "guest-login-post": "Log in to post", "no_topics": "此版块还没有任何内容。
赶紧来发帖吧!", "browsing": "正在浏览", "no_replies": "尚无回复", diff --git a/public/language/zh_CN/email.json b/public/language/zh_CN/email.json index e81b810a9a..9f0316ce3b 100644 --- a/public/language/zh_CN/email.json +++ b/public/language/zh_CN/email.json @@ -9,9 +9,9 @@ "reset.text1": "我们收到了重置您帐户密码的申请,可能是因为您遗忘了密码。如果不是,请忽略这封邮件。", "reset.text2": "如需继续重置密码,请点击下面的链接:", "reset.cta": "点击这里重置您的密码", - "reset.notify.subject": "Password successfully changed", - "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", - "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "reset.notify.subject": "更改密码成功", + "reset.notify.text1": "我们注意到你在 %1 上,成功修改了你的密码。", + "reset.notify.text2": "如果你没有授权此操作,请立即联系管理员。", "digest.notifications": "您有来自 %1 的未读通知:", "digest.latest_topics": "来自 %1 的最新主题", "digest.cta": "点击这里访问 %1", @@ -20,8 +20,8 @@ "notif.chat.subject": "收到来自 %1 的新聊天消息", "notif.chat.cta": "点击这里恢复会话", "notif.chat.unsub.info": "根据您的订阅设置,为您发送此聊天提醒。", - "notif.post.cta": "Click here to read the full topic", - "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.cta": "点击这里阅读全主题。", + "notif.post.unsub.info": "根据您的订阅设置,为您发送此回帖提醒。", "test.text1": "这是一封测试邮件,用来验证 NodeBB 的邮件配置是否设置正确。", "unsub.cta": "点击这里修改这些设置", "closing": "谢谢!" diff --git a/public/language/zh_CN/error.json b/public/language/zh_CN/error.json index cfcb33aee8..2511027169 100644 --- a/public/language/zh_CN/error.json +++ b/public/language/zh_CN/error.json @@ -18,7 +18,7 @@ "username-taken": "用户名已被占用", "email-taken": "电子邮箱已被占用", "email-not-confirmed": "您的电子邮箱尚未确认,请点击这里确认您的电子邮箱。", - "email-not-confirmed-chat": "You are unable to chat until your email is confirmed", + "email-not-confirmed-chat": "在确认您的邮箱之前,您不能使用聊天功能", "username-too-short": "用户名太短", "username-too-long": "用户名太长", "user-banned": "用户已禁止", @@ -35,7 +35,7 @@ "topic-locked": "主题已锁定", "still-uploading": "请等待上传完成", "content-too-short": "请再输入一些内容,帖子至少要有 %1 个字符。", - "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 characters.", + "content-too-long": "请输入更短的发帖。发帖字数不能超过 %1 个字符。", "title-too-short": "请再输入一些内容,标题至少要有 %1 个字符。", "title-too-long": "请输入更短的标题。不超过 %1 字。", "too-many-posts": "发帖间隔至少要 %1 秒 - 请稍候再发帖", @@ -45,13 +45,13 @@ "already-favourited": "您已收藏该帖", "already-unfavourited": "您已取消收藏此帖", "cant-ban-other-admins": "您不能禁止其他管理员!", - "invalid-image-type": "Invalid image type. Allowed types are: %1", - "invalid-image-extension": "Invalid image extension", + "invalid-image-type": "无效的图像类型。允许的类型有:%1", + "invalid-image-extension": "无效的图像扩展", "group-name-too-short": "用户组名称太短", "group-already-exists": "用户组已存在", "group-name-change-not-allowed": "不允许更改用户组名称", - "group-already-member": "You are already part of this group", - "group-needs-owner": "This group requires at least one owner", + "group-already-member": "您已是此小组成员", + "group-needs-owner": "此小组需要至少一名组长", "post-already-deleted": "此帖子已被删除", "post-already-restored": "此帖已经恢复", "topic-already-deleted": "此主题已被删除", @@ -63,12 +63,12 @@ "signature-too-long": "抱歉,您的签名不能超过 %1 个字符。", "cant-chat-with-yourself": "您不能和自己聊天!", "chat-restricted": "此用户限制了他的聊天消息。必须他先关注您,您才能和他聊天。", - "too-many-messages": "You have sent too many messages, please wait awhile.", + "too-many-messages": "您发送了太多消息,请稍等片刻。", "reputation-system-disabled": "威望系统已禁用。", "downvoting-disabled": "反对功能已禁用", "not-enough-reputation-to-downvote": "您还没有足够的威望为此帖扣分", "not-enough-reputation-to-flag": "您没有足够的威望标记此帖", "reload-failed": "NodeBB 重新加载时遇到问题: \"%1\"。NodeBB 会继续给已存在的客户端组件服务,虽然您应该撤销在重新加载前执行的操作。", "registration-error": "注册错误", - "parse-error": "Something went wrong while parsing server response" + "parse-error": "解析服务器响应时出错" } \ No newline at end of file diff --git a/public/language/zh_CN/global.json b/public/language/zh_CN/global.json index d8b6b8ea01..ede930cbbf 100644 --- a/public/language/zh_CN/global.json +++ b/public/language/zh_CN/global.json @@ -3,10 +3,10 @@ "search": "搜索", "buttons.close": "关闭", "403.title": "禁止访问", - "403.message": "You seem to have stumbled upon a page that you do not have access to.", - "403.login": "Perhaps you should try logging in?", + "403.message": "您遇到了没有权限访问的页面。", + "403.login": "或许您应该 试试登入?", "404.title": "未找到", - "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "404.message": "您遇到了不存在的页面。返回首页。", "500.title": "内部错误。", "500.message": "哎呀!看来是哪里出错了!", "register": "注册", @@ -27,7 +27,7 @@ "header.tags": "话题", "header.popular": "热门", "header.users": "会员", - "header.groups": "Groups", + "header.groups": "小组", "header.chats": "聊天", "header.notifications": "通知", "header.search": "搜索", @@ -75,7 +75,7 @@ "updated.title": "论坛已更新", "updated.message": "论坛已更新到最新版本。点这里刷新页面。", "privacy": "隐私", - "follow": "Follow", - "unfollow": "Unfollow", + "follow": "关注", + "unfollow": "取消关注", "delete_all": "全部删除" } \ No newline at end of file diff --git a/public/language/zh_CN/groups.json b/public/language/zh_CN/groups.json index 5a4bdd5b1f..bd0abbbf67 100644 --- a/public/language/zh_CN/groups.json +++ b/public/language/zh_CN/groups.json @@ -1,23 +1,23 @@ { "groups": "用户组", "view_group": "查看用户组", - "owner": "Group Owner", - "new_group": "Create New Group", - "no_groups_found": "There are no groups to see", - "cover-instructions": "Drag and Drop a photo, drag to position, and hit Save", - "cover-change": "Change", - "cover-save": "Save", - "cover-saving": "Saving", + "owner": "用户组组长", + "new_group": "创建新用户组", + "no_groups_found": "还没有用户组", + "cover-instructions": "拖放照片,拖动位置,然后点击 保存", + "cover-change": "变更", + "cover-save": "保存", + "cover-saving": "正在保存", "details.title": "用户组详情", "details.members": "会员列表", - "details.pending": "Pending Members", + "details.pending": "预备成员", "details.has_no_posts": "此用户组的会员尚未发表任何帖子。", "details.latest_posts": "最新帖子", - "details.private": "Private Group", - "details.public": "Public Group", - "details.grant": "Grant/Rescind Ownership", - "details.kick": "Kick", - "details.owner_options": "Group Administration", - "event.updated": "Group details have been updated", - "event.deleted": "The group \"%1\" has been deleted" + "details.private": "私有组", + "details.public": "公共组", + "details.grant": "授予/取消所有权", + "details.kick": "踢", + "details.owner_options": "用户组管理", + "event.updated": "用户组信息已更新", + "event.deleted": "用户组 \"%1\" 已被删除" } \ No newline at end of file diff --git a/public/language/zh_CN/pages.json b/public/language/zh_CN/pages.json index 36d10c1cb3..6fde75dfe5 100644 --- a/public/language/zh_CN/pages.json +++ b/public/language/zh_CN/pages.json @@ -11,7 +11,7 @@ "user.followers": "关注 %1 的人", "user.posts": "%1 发布的帖子", "user.topics": "%1 创建的主题", - "user.groups": "%1's Groups", + "user.groups": "%1 的用户组", "user.favourites": "%1 收藏的帖子", "user.settings": "用户设置", "maintenance.text": "%1 正在进行维护。请稍后再来。", diff --git a/public/language/zh_CN/recent.json b/public/language/zh_CN/recent.json index d3fea3da46..8aa1e96ab2 100644 --- a/public/language/zh_CN/recent.json +++ b/public/language/zh_CN/recent.json @@ -6,7 +6,7 @@ "year": "年度热帖榜", "alltime": "总热帖榜", "no_recent_topics": "暂无主题。", - "there-is-a-new-topic": "There is a new topic.", + "there-is-a-new-topic": "这是个新主题。", "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", "there-are-new-topics": "There are %1 new topics.", @@ -14,5 +14,5 @@ "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", "there-is-a-new-post": "There is a new post.", "there-are-new-posts": "There are %1 new posts.", - "click-here-to-reload": "Click here to reload." + "click-here-to-reload": "点击这里重新加载" } \ No newline at end of file diff --git a/public/language/zh_CN/reset_password.json b/public/language/zh_CN/reset_password.json index ca294f9dc7..9b0ca29dbe 100644 --- a/public/language/zh_CN/reset_password.json +++ b/public/language/zh_CN/reset_password.json @@ -11,6 +11,6 @@ "enter_email_address": "输入邮箱地址", "password_reset_sent": "密码重置邮件已发送。", "invalid_email": "无效的电子邮箱/电子邮箱不存在!", - "password_too_short": "The password entered is too short, please pick a different password.", - "passwords_do_not_match": "The two passwords you've entered do not match." + "password_too_short": "密码太短,请选择其他密码。", + "passwords_do_not_match": "您输入两个密码不一致。" } \ No newline at end of file diff --git a/public/language/zh_CN/search.json b/public/language/zh_CN/search.json index 25cc3605e5..1b1096b667 100644 --- a/public/language/zh_CN/search.json +++ b/public/language/zh_CN/search.json @@ -1,35 +1,35 @@ { "results_matching": "共 %1 条结果匹配 \"%2\",(耗时 %3 秒)", - "no-matches": "No matches found", - "in": "In", - "by": "By", - "titles": "Titles", - "titles-posts": "Titles and Posts", - "posted-by": "Posted by", - "in-categories": "In Categories", - "search-child-categories": "Search child categories", - "reply-count": "Reply Count", - "at-least": "At least", - "at-most": "At most", - "post-time": "Post time", - "newer-than": "Newer than", - "older-than": "Older than", - "any-date": "Any date", - "yesterday": "Yesterday", - "one-week": "One week", - "two-weeks": "Two weeks", - "one-month": "One month", - "three-months": "Three months", - "six-months": "Six months", - "one-year": "One year", - "sort-by": "Sort by", - "last-reply-time": "Last reply time", - "topic-title": "Topic title", - "number-of-replies": "Number of replies", - "number-of-views": "Number of views", - "topic-start-date": "Topic start date", - "username": "Username", - "category": "Category", - "descending": "In descending order", - "ascending": "In ascending order" + "no-matches": "无匹配结果", + "in": "在", + "by": "-", + "titles": "标题", + "titles-posts": "标题和回帖", + "posted-by": "发表", + "in-categories": "在版面", + "search-child-categories": "搜索子版面", + "reply-count": "回复数", + "at-least": "至少", + "at-most": "至多", + "post-time": "发帖时间", + "newer-than": "晚于", + "older-than": "早于", + "any-date": "任何日期", + "yesterday": "昨天", + "one-week": "一周", + "two-weeks": "两周", + "one-month": "一个月", + "three-months": "三个月", + "six-months": "六个月", + "one-year": "一年", + "sort-by": "排序", + "last-reply-time": "最后回复时间", + "topic-title": "主题标题", + "number-of-replies": "回帖数", + "number-of-views": "查看数", + "topic-start-date": "主题开始日期", + "username": "用户名", + "category": "版面", + "descending": "逆序", + "ascending": "顺序" } \ No newline at end of file diff --git a/public/language/zh_CN/topic.json b/public/language/zh_CN/topic.json index 8dc93ec5f5..bd20fbdabc 100644 --- a/public/language/zh_CN/topic.json +++ b/public/language/zh_CN/topic.json @@ -12,6 +12,7 @@ "notify_me": "此主题有新回复时通知我", "quote": "引用", "reply": "回复", + "guest-login-reply": "Log in to reply", "edit": "编辑", "delete": "删除", "purge": "清除", @@ -74,7 +75,7 @@ "fork_no_pids": "未选中帖子!", "fork_success": "成功分割主题! 点这里跳转到分割后的主题。", "composer.title_placeholder": "在此输入您主题的标题...", - "composer.handle_placeholder": "Name", + "composer.handle_placeholder": "姓名", "composer.discard": "撤销", "composer.submit": "提交", "composer.replying_to": "正在回复 %1", @@ -94,5 +95,5 @@ "oldest_to_newest": "从旧到新", "newest_to_oldest": "从新到旧", "most_votes": "最多投票", - "most_posts": "Most posts" + "most_posts": "最多发表" } \ No newline at end of file diff --git a/public/language/zh_CN/user.json b/public/language/zh_CN/user.json index 9379b69e38..6c360a4d4e 100644 --- a/public/language/zh_CN/user.json +++ b/public/language/zh_CN/user.json @@ -2,8 +2,8 @@ "banned": "禁止", "offline": "离线", "username": "用户名", - "joindate": "Join Date", - "postcount": "Post Count", + "joindate": "注册日期", + "postcount": "发帖数", "email": "电子邮件", "confirm_email": "确认电子邮箱", "delete_account": "删除帐号", @@ -18,7 +18,7 @@ "profile_views": "资料浏览", "reputation": "威望", "favourites": "收藏的帖子", - "watched": "Watched", + "watched": "已订阅", "followers": "粉丝", "following": "关注", "signature": "签名档", @@ -59,12 +59,12 @@ "digest_weekly": "每周", "digest_monthly": "每月", "send_chat_notifications": "当我不在线,并受到新的聊天消息时给我发邮件", - "send_post_notifications": "Send an email when replies are made to topics I am subscribed to", + "send_post_notifications": "我订阅的主题有回复时发送邮件", "has_no_follower": "此用户还没有粉丝 :(", "follows_no_one": "此用户尚未关注任何人 :(", "has_no_posts": "此用户尚未发布任何帖子。", "has_no_topics": "此用户还未发布任何主题。", - "has_no_watched_topics": "This user didn't watch any topics yet.", + "has_no_watched_topics": "此用户还未订阅任何主题", "email_hidden": "电子邮箱已隐藏", "hidden": "隐藏", "paginate_description": "分页展示主题和帖子,替代滚动展示。", diff --git a/public/language/zh_CN/users.json b/public/language/zh_CN/users.json index f56a4fb88e..ef7d6dd15b 100644 --- a/public/language/zh_CN/users.json +++ b/public/language/zh_CN/users.json @@ -5,8 +5,8 @@ "search": "搜索", "enter_username": "输入用户名搜索", "load_more": "加载更多", - "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", - "filter-by": "Filter By", - "online-only": "Online only", - "picture-only": "Picture only" + "users-found-search-took": "找到 %1 位用户!搜索耗时 %2 毫秒。", + "filter-by": "过滤选项", + "online-only": "只看在线", + "picture-only": "只看图片" } \ No newline at end of file diff --git a/public/language/zh_TW/category.json b/public/language/zh_TW/category.json index cb21821aab..055d4a70d3 100644 --- a/public/language/zh_TW/category.json +++ b/public/language/zh_TW/category.json @@ -1,5 +1,6 @@ { "new_topic_button": "新主題", + "guest-login-post": "Log in to post", "no_topics": "這個版面還沒有任何內容。
趕緊來發文章吧!", "browsing": "正在瀏覽", "no_replies": "還沒有回覆", diff --git a/public/language/zh_TW/topic.json b/public/language/zh_TW/topic.json index 04787766fa..41edd8828f 100644 --- a/public/language/zh_TW/topic.json +++ b/public/language/zh_TW/topic.json @@ -12,6 +12,7 @@ "notify_me": "該主題有新回覆時通知我", "quote": "引用", "reply": "回覆", + "guest-login-reply": "Log in to reply", "edit": "編輯", "delete": "刪除", "purge": "清除", diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index ee9a87b605..1e4a10643d 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -10,6 +10,7 @@ @import "./appearance/customise"; @import "./appearance/themes"; @import "./extend/plugins"; +@import "./extend/rewards"; @import "./advanced/database"; @import "./modules/alerts"; @@ -183,15 +184,18 @@ .box-header-font } - #user_dropdown { - padding: 6px; - - img { - width: 30px; - height: 30px; - vertical-align: -88%; - margin-right: 5px; - } + #user_label { + a { + padding-top: 13px; + padding-bottom: 13px; + + img { + width: 24px; + height: 24px; + border-radius: 50%; + border: 1px solid #454; + } + } } .icon-container { @@ -213,24 +217,39 @@ } } - .navbar { - padding: 0 5px; - - .nav-home a, .nav-home a:hover { - width: 30px; - height: 30px; - padding: 5px; - text-align: center; - margin-top: 10px; - background: #DDD; - - i { - color: black; - font-size: 17px; - } - } - - } + .navbar-static-top, .navbar-fixed-top { + box-shadow: 0px -3px 12px rgba(0, 0, 0, 0.5); + } + + .navbar-header > .navbar-toggle { + margin-right: 8px; + } + + .navbar-nav { + margin-top: 0; + margin-bottom: 0; + + >li { + >a { + padding-top: 15px; + padding-bottom: 15px; + } + + >a:hover, >a:focus { + color: @gray-dark; + background-color: @gray-light; + } + + >#reconnect { + color: @gray-light; + } + + >#reconnect:focus, >#reconnect:hover { + color: @gray-light; + background-color: transparent; + } + } + } #acp-search { input { diff --git a/public/less/admin/advanced/database.less b/public/less/admin/advanced/database.less index c81ef613cc..3799ced20a 100644 --- a/public/less/admin/advanced/database.less +++ b/public/less/admin/advanced/database.less @@ -3,4 +3,21 @@ display:inline-block; width:220px; } -} \ No newline at end of file +} + + + + + + + + + + + + + + + + + diff --git a/public/less/admin/extend/rewards.less b/public/less/admin/extend/rewards.less new file mode 100644 index 0000000000..2dc84b5bc3 --- /dev/null +++ b/public/less/admin/extend/rewards.less @@ -0,0 +1,21 @@ +#rewards { + .well, .panel-body { + vertical-align: top; + min-height: 100px; + + &.pull-right { + min-height: 0px; + } + } + + ul { + list-style-type: none; + padding: 0px; + margin: 0px; + + > li { + border-bottom: 1px solid #ddd; + margin-bottom: 20px; + } + } +} \ No newline at end of file diff --git a/public/src/admin/appearance/skins.js b/public/src/admin/appearance/skins.js index 8fd9208c7e..df21cb5b0a 100644 --- a/public/src/admin/appearance/skins.js +++ b/public/src/admin/appearance/skins.js @@ -58,7 +58,8 @@ define('admin/appearance/skins', function() { url: theme.preview, css: theme.cssCdn }; - }) + }), + showRevert: true }, function(html) { themeContainer.html(html); }); diff --git a/public/src/admin/extend/plugins.js b/public/src/admin/extend/plugins.js index 66ffc19d7f..220c4e1980 100644 --- a/public/src/admin/extend/plugins.js +++ b/public/src/admin/extend/plugins.js @@ -93,6 +93,38 @@ define('admin/extend/plugins', function() { $(this).toggleClass('hide', pluginId && pluginId.indexOf(term) === -1); }); }); + + $('#plugin-order').on('click', function() { + $('#order-active-plugins-modal').modal('show'); + socket.emit('admin.plugins.getActive', function(err, activePlugins) { + if (err) { + return app.alertError(err); + } + var html = ''; + activePlugins.forEach(function(plugin) { + html += '

  • ' + plugin + '
  • '; + }); + if (!activePlugins.length) { + html = 'No Active Plugins'; + } + $('#order-active-plugins-modal .plugin-list').html(html).sortable(); + }); + }); + + $('#save-plugin-order').on('click', function() { + var plugins = $('#order-active-plugins-modal .plugin-list').children(); + var data = []; + plugins.each(function(index, el) { + data.push({name: $(el).text(), order: index}); + }); + + socket.emit('admin.plugins.orderActivePlugins', data, function(err) { + if (err) { + return app.alertError(err.message); + } + $('#order-active-plugins-modal').modal('hide'); + }); + }); }; function confirmInstall(pluginID, callback) { diff --git a/public/src/admin/extend/rewards.js b/public/src/admin/extend/rewards.js new file mode 100644 index 0000000000..967e098bf7 --- /dev/null +++ b/public/src/admin/extend/rewards.js @@ -0,0 +1,183 @@ +"use strict"; +/* global define, app, ajaxify, socket, templates, bootbox */ + +define('admin/extend/rewards', function() { + var rewards = {}; + + + var available, + active, + conditions, + conditionals; + + rewards.init = function() { + available = JSON.parse(ajaxify.variables.get('rewards')); + active = JSON.parse(ajaxify.variables.get('active')); + conditions = JSON.parse(ajaxify.variables.get('conditions')); + conditionals = JSON.parse(ajaxify.variables.get('conditionals')); + + $('[data-selected]').each(function() { + select($(this)); + }); + + $('#active') + .on('change', '[data-selected]', function() { + update($(this)); + }) + .on('click', '.delete', function() { + var parent = $(this).parents('[data-id]'), + id = parent.attr('data-id'); + + socket.emit('admin.rewards.delete', {id: id}, function(err) { + if (err) { + app.alertError(err.message); + } else { + app.alertSuccess('Successfully deleted reward'); + } + }); + + parent.remove(); + return false; + }) + .on('click', '.toggle', function() { + var btn = $(this), + disabled = btn.html() === 'Enable', + id = $(this).parents('[data-id]').attr('data-id'); + + btn.toggleClass('btn-warning').toggleClass('btn-success').html(disabled ? 'Enable' : 'Disable'); + // send disable api call + return false; + }); + + $('#new').on('click', newReward); + $('#save').on('click', saveRewards); + + populateInputs(); + }; + + function select(el) { + el.val(el.attr('data-selected')); + switch (el.attr('name')) { + case 'rid': + selectReward(el); + break; + } + } + + function update(el) { + el.attr('data-selected', el.val()); + switch (el.attr('name')) { + case 'rid': + selectReward(el); + break; + } + } + + function selectReward(el) { + var parent = el.parents('[data-rid]'), + div = parent.find('.inputs'), + inputs, + html = ''; + + for (var reward in available) { + if (available.hasOwnProperty(reward)) { + if (available[reward].rid === el.attr('data-selected')) { + inputs = available[reward].inputs; + parent.attr('data-rid', available[reward].rid); + break; + } + } + } + + if (!inputs) { + return app.alertError('Illegal reward - no inputs found! ' + el.attr('data-selected')); + } + + inputs.forEach(function(input) { + html += '
    '; + }); + + div.html(html); + } + + function populateInputs() { + $('[data-rid]').each(function(i) { + var div = $(this).find('.inputs'), + rewards = active[i].rewards; + + for (var reward in rewards) { + if (rewards.hasOwnProperty(reward)) { + div.find('[name="' + reward + '"]').val(rewards[reward]); + } + } + }); + } + + function newReward() { + var ul = $('#active'); + + var data = { + active: [{ + disabled: true, + value: '', + claimable: 1, + rid: null, + id: null + }], + conditions: conditions, + conditionals: conditionals, + rewards: available, + }; + + templates.parse('admin/extend/rewards', 'active', data, function(li) { + li = $(li); + ul.append(li); + li.find('select').val(''); + }); + } + + function saveRewards() { + var activeRewards = []; + + $('#active li').each(function() { + var data = {rewards: {}}, + main = $(this).find('form.main').serializeArray(), + rewards = $(this).find('form.rewards').serializeArray(); + + main.forEach(function(obj) { + data[obj.name] = obj.value; + }); + + rewards.forEach(function(obj) { + data.rewards[obj.name] = obj.value; + }); + + data.id = $(this).attr('data-id'); + data.disabled = $(this).find('.toggle').html() === 'Enable'; + + activeRewards.push(data); + }); + + socket.emit('admin.rewards.save', activeRewards, function(err) { + if (err) { + app.alertError(err.message); + } else { + app.alertSuccess('Successfully saved rewards'); + } + }); + } + + return rewards; +}); \ No newline at end of file diff --git a/public/src/admin/extend/widgets.js b/public/src/admin/extend/widgets.js index 9632bbfa68..43f70de05a 100644 --- a/public/src/admin/extend/widgets.js +++ b/public/src/admin/extend/widgets.js @@ -80,7 +80,14 @@ define('admin/extend/widgets', function() { for (var d in data) { if (data.hasOwnProperty(d)) { if (data[d].name) { - widgetData[data[d].name] = data[d].value; + if (widgetData[data[d].name]) { + if(!Array.isArray(widgetData[data[d].name])) { + widgetData[data[d].name] = [ widgetData[data[d].name] ]; + } + widgetData[data[d].name].push(data[d].value); + }else{ + widgetData[data[d].name] = data[d].value; + } } } } @@ -162,7 +169,7 @@ define('admin/extend/widgets', function() { title.text(title.text() + ' - ' + data.title); } - widget.find('input, textarea').each(function() { + widget.find('input, textarea, select').each(function() { var input = $(this), value = data[input.attr('name')]; diff --git a/public/src/admin/manage/flags.js b/public/src/admin/manage/flags.js index c6dc4b11af..880094af20 100644 --- a/public/src/admin/manage/flags.js +++ b/public/src/admin/manage/flags.js @@ -1,12 +1,21 @@ "use strict"; /*global define, socket, app, admin, utils, bootbox, RELATIVE_PATH*/ -define('admin/manage/flags', ['forum/infinitescroll', 'admin/modules/selectable'], function(infinitescroll, selectable) { +define('admin/manage/flags', [ + 'forum/infinitescroll', + 'admin/modules/selectable', + 'autocomplete' +], function(infinitescroll, selectable, autocomplete) { + var Flags = {}; Flags.init = function() { $('.post-container .content img').addClass('img-responsive'); + var params = utils.params(); + $('#flag-sort-by').val(params.sortBy); + autocomplete.user($('#byUsername')); + handleDismiss(); handleDismissAll(); handleDelete(); @@ -69,8 +78,15 @@ define('admin/manage/flags', ['forum/infinitescroll', 'admin/modules/selectable' if (direction < 0 && !$('.flags').length) { return; } + var params = utils.params(); + var sortBy = params.sortBy || 'count'; + var byUsername = params.byUsername || ''; - infinitescroll.loadMore('admin.getMoreFlags', $('[data-next]').attr('data-next'), function(data, done) { + infinitescroll.loadMore('admin.getMoreFlags', { + byUsername: byUsername, + sortBy: sortBy, + after: $('[data-next]').attr('data-next') + }, function(data, done) { if (data.posts && data.posts.length) { infinitescroll.parseAndTranslate('admin/manage/flags', 'posts', {posts: data.posts}, function(html) { $('[data-next]').attr('data-next', data.next); diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js index b2ae0c0afb..ed8cc59683 100644 --- a/public/src/admin/manage/groups.js +++ b/public/src/admin/manage/groups.js @@ -1,5 +1,5 @@ "use strict"; -/*global define, templates, socket, ajaxify, app, admin, bootbox*/ +/*global define, templates, socket, ajaxify, app, admin, bootbox, utils, config, translator */ define('admin/manage/groups', [ 'iconSelect', @@ -50,19 +50,13 @@ define('admin/manage/groups', [ socket.emit('admin.groups.create', submitObj, function(err) { if (err) { - switch (err) { - case 'group-exists': - errorText = 'Please choose another name

    There seems to be a group with this name already.

    '; - break; - case 'name-too-short': - errorText = 'Please specify a group name

    A group name is required for administrative purposes.

    '; - break; - default: - errorText = 'Uh-Oh

    There was a problem creating your group. Please try again later!

    '; - break; + if (err.hasOwnProperty('message') && utils.hasLanguageKey(err.message)) { + translator.translate(err.message, config.defaultLang, function(translated) { + createModalError.html(translated).removeClass('hide'); + }); + } else { + createModalError.html('Uh-Oh

    There was a problem creating your group. Please try again later!

    ').removeClass('hide'); } - - createModalError.html(errorText).removeClass('hide'); } else { createModalError.addClass('hide'); createGroupName.val(''); diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index dad64e35af..d9eb888985 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -214,7 +214,7 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) } $('#create-modal').modal('hide'); $('#create-modal').on('hidden.bs.modal', function() { - ajaxify.go('admin/users'); + ajaxify.refresh(); }); app.alertSuccess('User created!'); }); diff --git a/public/src/app.js b/public/src/app.js index 5010ead25d..d3a47a1bc6 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -70,6 +70,10 @@ app.cacheBuster = null; window.location.href = config.relative_path + '/'; }, 1000); }); + + socket.on('event:alert', function(data) { + app.alert(data); + }); } function onSocketConnect(data) { @@ -566,7 +570,21 @@ app.cacheBuster = null; }; function showEmailConfirmWarning() { - if (config.requireEmailConfirmation && app.user.uid && !app.user['email:confirmed']) { + if (!config.requireEmailConfirmation || !app.user.uid) { + return; + } + if (!app.user.email) { + app.alert({ + alert_id: 'email_confirm', + message: '[[error:no-email-to-confirm]]', + type: 'warning', + timeout: 0, + clickfn: function() { + app.removeAlert('email_confirm'); + ajaxify.go('user/' + app.user.userslug + '/edit'); + } + }); + } else if (!app.user['email:confirmed']) { app.alert({ alert_id: 'email_confirm', message: '[[error:email-not-confirmed]]', diff --git a/public/src/client/home.js b/public/src/client/categories.js similarity index 67% rename from public/src/client/home.js rename to public/src/client/categories.js index 23049af741..6840f69e89 100644 --- a/public/src/client/home.js +++ b/public/src/client/categories.js @@ -2,35 +2,34 @@ /* globals define, socket, app, templates, translator, ajaxify*/ -define('forum/home', function() { - var home = {}; +define('forum/categories', function() { + var categories = {}; $(window).on('action:ajaxify.start', function(ev, data) { - if (data.tpl_url !== 'home') { - socket.removeListener('event:new_post', home.onNewPost); + if (data.tpl_url !== 'categories') { + socket.removeListener('event:new_post', categories.onNewPost); } }); + categories.init = function() { + app.enterRoom('categories'); - home.init = function() { - app.enterRoom('home'); + socket.removeListener('event:new_post', categories.onNewPost); + socket.on('event:new_post', categories.onNewPost); - socket.removeListener('event:new_post', home.onNewPost); - socket.on('event:new_post', home.onNewPost); - - $('.home .category-header').tooltip({ + $('.category-header').tooltip({ placement: 'bottom' }); }; - home.onNewPost = function(data) { + categories.onNewPost = function(data) { if (data && data.posts && data.posts.length && data.posts[0].topic) { renderNewPost(data.posts[0].topic.cid, data.posts[0]); } }; function renderNewPost(cid, post) { - var category = $('.home .category-item[data-cid="' + cid + '"]'); + var category = $('.category-item[data-cid="' + cid + '"]'); if (!category.length) { return; } @@ -59,12 +58,12 @@ define('forum/home', function() { recentPosts.last().remove(); } - $(window).trigger('action:posts.loaded'); + $(window).trigger('action:posts.loaded', {posts: [post]}); }); } function parseAndTranslate(posts, callback) { - templates.parse('home', 'posts', {categories: {posts: posts}}, function(html) { + templates.parse('categories', 'posts', {categories: {posts: posts}}, function(html) { translator.translate(html, function(translatedHTML) { translatedHTML = $(translatedHTML); translatedHTML.find('img').addClass('img-responsive'); @@ -74,5 +73,5 @@ define('forum/home', function() { }); } - return home; + return categories; }); diff --git a/public/src/client/category.js b/public/src/client/category.js index b9ab5bc800..2dccb5f85d 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -339,7 +339,7 @@ define('forum/category', [ infinitescroll.loadMore('categories.loadMore', { cid: ajaxify.variables.get('category_id'), after: after, - author: utils.getQueryParams().author + author: utils.params().author }, function (data, done) { if (data.topics && data.topics.length) { diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 1d651fad27..f4f56a686e 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -64,6 +64,8 @@ define('forum/groups/details', ['iconSelect', 'vendor/colorpicker/colorpicker', case 'leave': case 'accept': case 'reject': + case 'acceptInvite': + case 'rejectInvite': socket.emit('groups.' + action, { toUid: uid, groupName: ajaxify.variables.get('group_name') diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js index d0b4c69b50..2fe5caab95 100644 --- a/public/src/client/groups/list.js +++ b/public/src/client/groups/list.js @@ -47,6 +47,7 @@ define('forum/groups/list', function() { query: queryEl.val(), options: { expand: true, + truncateUserList: true, sort: sortEl.val() } }, function(err, groups) { diff --git a/public/src/client/notifications.js b/public/src/client/notifications.js index b0fbb4619f..fb778ec18f 100644 --- a/public/src/client/notifications.js +++ b/public/src/client/notifications.js @@ -1,16 +1,25 @@ +'use strict'; + +/* globals define, socket, app */ + define('forum/notifications', function() { var Notifications = {}; Notifications.init = function() { var listEl = $('.notifications-list'); - listEl.on('click', 'li', function(e) { - this.querySelector('a').click(); + listEl.on('click', 'a', function(e) { + var nid = $(this).parents('[data-nid]').attr('data-nid'); + socket.emit('notifications.markRead', nid, function(err) { + if (err) { + return app.alertError(err); + } + }); }); $('span.timeago').timeago(); $('.notifications .delete').on('click', function() { - socket.emit('notifications.deleteAll', function(err) { + socket.emit('notifications.markAllRead', function(err) { if (err) { return app.alertError(err.message); } @@ -20,8 +29,7 @@ define('forum/notifications', function() { listEl.empty(); }); }); - - } + }; return Notifications; }); diff --git a/public/src/client/pagination.js b/public/src/client/pagination.js index 3d306e9f65..e2df0a6ebe 100644 --- a/public/src/client/pagination.js +++ b/public/src/client/pagination.js @@ -20,16 +20,14 @@ define('forum/pagination', function() { }; pagination.loadPage = function(page, callback) { + callback = callback || function() {}; page = parseInt(page, 10); - if(!utils.isNumber(page) || page < 1 || page > pagination.pageCount) { + if (!utils.isNumber(page) || page < 1 || page > pagination.pageCount) { + callback(false); return false; } - - ajaxify.go(window.location.pathname.slice(1) + '?page=' + page, function() { - if (typeof callback === 'function') { - callback(); - } - }); + var url = window.location.pathname.slice(1).split('/').slice(0, 3).join('/') + '?page=' + page; + ajaxify.go(url, callback); return true; }; diff --git a/public/src/client/popular.js b/public/src/client/popular.js index 6edc5a071c..0d05186faf 100644 --- a/public/src/client/popular.js +++ b/public/src/client/popular.js @@ -2,16 +2,12 @@ /* globals define, app, socket*/ -define('forum/popular', ['forum/recent', 'forum/infinitescroll'], function(recent, infinitescroll) { +define('forum/popular', ['forum/recent'], function(recent) { var Popular = {}; Popular.init = function() { app.enterRoom('recent_posts'); - $('#new-topics-alert').on('click', function() { - $(this).addClass('hide'); - }); - selectActivePill(); }; @@ -26,10 +22,10 @@ define('forum/popular', ['forum/recent', 'forum/infinitescroll'], function(recen return false; } }); - }; + } function getActiveSection() { - parts = window.location.href.split('/'); + var parts = window.location.href.split('/'); return parts[parts.length - 1]; } diff --git a/public/src/client/search.js b/public/src/client/search.js index a8924ce8f9..56bdaf6dca 100644 --- a/public/src/client/search.js +++ b/public/src/client/search.js @@ -2,7 +2,7 @@ /* globals app, define, utils, socket*/ -define('forum/search', ['search'], function(searchModule) { +define('forum/search', ['search', 'autocomplete'], function(searchModule, autocomplete) { var Search = {}; Search.init = function() { @@ -44,7 +44,7 @@ define('forum/search', ['search'], function(searchModule) { in: form.find('#search-in').val() }; - if (searchData.in === 'posts' || searchData.in === 'titlespost' || searchData.in === 'titles') { + if (searchData.in === 'posts' || searchData.in === 'titlesposts' || searchData.in === 'titles') { searchData.by = form.find('#posted-by-user').val(); searchData.categories = form.find('#posted-in-categories').val(); searchData.searchChildren = form.find('#search-children').is(':checked'); @@ -80,8 +80,8 @@ define('forum/search', ['search'], function(searchModule) { $('#posted-by-user').val(params.by); } - if ((params['categories[]'] || params.categories)) { - $('#posted-in-categories').val(params['categories[]'] || params.categories); + if (params.categories) { + $('#posted-in-categories').val(params.categories); } if (params.searchChildren) { @@ -156,25 +156,7 @@ define('forum/search', ['search'], function(searchModule) { } function enableAutoComplete() { - var input = $('#posted-by-user'); - input.autocomplete({ - delay: 100, - source: function(request, response) { - socket.emit('user.search', {query: request.term}, function(err, result) { - if (err) { - return app.alertError(err.message); - } - - if (result && result.users) { - var names = result.users.map(function(user) { - return user && user.username; - }); - response(names); - } - $('.ui-autocomplete a').attr('data-ajaxify', 'false'); - }); - } - }); + autocomplete.user($('#posted-by-user')); } return Search; diff --git a/public/src/client/topic/browsing.js b/public/src/client/topic/browsing.js index dd768391e1..935439fa88 100644 --- a/public/src/client/topic/browsing.js +++ b/public/src/client/topic/browsing.js @@ -9,10 +9,10 @@ define('forum/topic/browsing', function() { var Browsing = {}; Browsing.onUpdateUsersInRoom = function(data) { - if(data && data.room.indexOf('topic_' + ajaxify.variables.get('topic_id')) !== -1) { + if (data && data.room.indexOf('topic_' + ajaxify.variables.get('topic_id')) !== -1) { $('.browsing-users').toggleClass('hidden', !data.users.length); for(var i=0; i'); - } + for(var x=0,numButtons=customButtons.length;x'); } - } + }; formatting.addButton = function(iconClass, onClick) { - formattingDispatchTable[iconClass] = onClick; + var name = iconClass.replace('fa fa-', ''); + + formattingDispatchTable[name] = onClick; customButtons.push({ + name: name, iconClass: iconClass }); - } + }; + + formatting.addButtonDispatch = function(name, onClick) { + formattingDispatchTable[name] = onClick; + }; formatting.addHandler = function(postContainer) { postContainer.on('click', '.formatting-bar span', function () { - var iconClass = $(this).find('i').attr('class'); - var textarea = $(this).parents('.composer').find('textarea')[0]; + var format = $(this).attr('data-format'), + textarea = $(this).parents('.composer').find('textarea')[0]; - if(formattingDispatchTable.hasOwnProperty(iconClass)){ - formattingDispatchTable[iconClass](textarea, textarea.selectionStart, textarea.selectionEnd); + if(formattingDispatchTable.hasOwnProperty(format)){ + formattingDispatchTable[format](textarea, textarea.selectionStart, textarea.selectionEnd); preview.render(postContainer); } }); diff --git a/public/src/modules/composer/uploads.js b/public/src/modules/composer/uploads.js index e7bc473cb5..2a6833c604 100644 --- a/public/src/modules/composer/uploads.js +++ b/public/src/modules/composer/uploads.js @@ -316,8 +316,7 @@ define('composer/uploads', ['composer/preview', 'csrf'], function(preview, csrf) function onUploadError(xhr) { xhr = maybeParse(xhr); - - app.alertError('[[error:upload-error, ' + xhr.responseText + ']]'); + app.alertError(xhr.responseText); } return uploads; diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index e8f77a5614..c4676a0e37 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -33,6 +33,8 @@ } else { if (groupObj.isPending) { return ''; + } else if (groupObj.isInvited) { + return ''; } else { return ''; } diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index 319d88041f..6e36cf7296 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -2,7 +2,6 @@ /* globals define, socket, translator, utils, config, app, ajaxify, Tinycon*/ - define('notifications', ['sounds'], function(sound) { var Notifications = {}; @@ -14,54 +13,65 @@ define('notifications', ['sounds'], function(sound) { notifTrigger.on('click', function(e) { e.preventDefault(); - if (!notifContainer.hasClass('open')) { - - socket.emit('notifications.get', null, function(err, data) { + if (notifContainer.hasClass('open')) { + return; + } - function createNotification(notification, callback) { - if (notification.image) { - image = ''; - } else { - image = ''; - } + socket.emit('notifications.get', null, function(err, data) { + if (err) { + return app.alertError(err.message); + } - return '
  • ' + image + '' + $.timeago(new Date(parseInt(notification.datetime, 10))) + '' + notification.bodyShort + '
  • '; - } + var notifs = data.unread.concat(data.read); - var x, html = ''; + translator.toggleTimeagoShorthand(); + for(var i=0; i 0) { - var image = ''; - for (x = 0; x < data.unread.length; x++) { - html += createNotification(data.unread[x]); - } + notifList.on('click', '[data-nid]', function() { + var nid = this.getAttribute('data-nid'); - for (x = 0; x < data.read.length; x++) { - html += createNotification(data.read[x]); - } - } else { - html += '
  • [[notifications:no_notifs]]
  • '; - } + socket.emit('notifications.markRead', nid, function(err) { + if (err) { + app.alertError(err.message); + } + }); + }); - // Switch back to original timeago strings - translator.toggleTimeagoShorthand(); + notifList.on('click', '.mark-all-read', function() { + socket.emit('notifications.markAllRead', function(err) { + if (err) { + app.alertError(err.message); + } + updateNotifCount(0); + }); + }); - html += ''; + notifList.on('click', '.mark-read', function(e) { + var anchorEl = $(this.parentNode), + parentEl = anchorEl.parent(), + nid = anchorEl.attr('data-nid'), + unread = parentEl.hasClass('unread'); - notifList.translateHtml(html); + e.preventDefault(); + e.stopPropagation(); - updateNotifCount(data.unread.length); + socket.emit('notifications.mark' + (unread ? 'Read' : 'Unread'), nid, function(err) { + if (err) { + app.alertError(err.message); + } - socket.emit('modules.notifications.markAllRead', null, function(err) { - if (!err) { - updateNotifCount(0); - } - }); - }); - } + parentEl.toggleClass('unread'); + increaseNotifCount(unread ? -1 : 1); + }); }); function updateNotifCount(count) { @@ -77,8 +87,8 @@ define('notifications', ['sounds'], function(sound) { Tinycon.setBubble(count); }; - function increaseNotifCount() { - var count = parseInt(notifIcon.attr('data-content'), 10) + 1; + function increaseNotifCount(delta) { + var count = parseInt(notifIcon.attr('data-content'), 10) + delta; updateNotifCount(count); } @@ -104,7 +114,7 @@ define('notifications', ['sounds'], function(sound) { ajaxify.refresh(); } - increaseNotifCount(); + increaseNotifCount(1); sound.play('notification'); }); diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js index 842cf7f8b6..75f32f80be 100644 --- a/public/src/modules/topicSelect.js +++ b/public/src/modules/topicSelect.js @@ -23,7 +23,7 @@ define('topicSelect', function() { return false; } - var isSelected = select.hasClass('fa-check-square-o'); + var isSelected = select.parents('[data-tid]').hasClass('selected'); toggleSelect(select, !isSelected); lastSelected = select; if (typeof onSelect === 'function') { diff --git a/public/src/overrides.js b/public/src/overrides.js index 978dea0720..913a5abf3f 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -1,5 +1,6 @@ 'use strict'; + if ('undefined' !== typeof window) { (function ($, undefined) { diff --git a/public/src/utils.js b/public/src/utils.js index 3b82573c9b..1521d53e28 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -71,9 +71,11 @@ trimTrailingDash: /-$/g, trimLeadingDash: /^-/g, isLatin: /^[\w]+$/, + languageKeyRegex: /\[\[[\w]+:.+\]\]/, //http://dense13.com/blog/2009/05/03/converting-string-to-slug-javascript/ slugify: function(str) { + if (!str) { str = ''; } str = str.replace(utils.trimRegex, ''); if(utils.isLatin.test(str)) { str = str.replace(utils.invalidLatinChars, '-'); @@ -108,6 +110,10 @@ return !isNaN(parseFloat(n)) && isFinite(n); }, + hasLanguageKey: function(input) { + return utils.languageKeyRegex.test(input); + }, + // shallow objects merge merge: function() { var result = {}, obj, keys; @@ -125,38 +131,39 @@ return ('' + path).split('.').pop(); }, - fileMimeType: (function () { - // we only care about images, for now - var map = { - "bmp": "image/bmp", - "cmx": "image/x-cmx", - "cod": "image/cis-cod", - "gif": "image/gif", - "ico": "image/x-icon", - "ief": "image/ief", - "jfif": "image/pipeg", - "jpe": "image/jpeg", - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "pbm": "image/x-portable-bitmap", - "pgm": "image/x-portable-graymap", - "pnm": "image/x-portable-anymap", - "ppm": "image/x-portable-pixmap", - "ras": "image/x-cmu-raster", - "rgb": "image/x-rgb", - "svg": "image/svg+xml", - "tif": "image/tiff", - "tiff": "image/tiff", - "xbm": "image/x-xbitmap", - "xpm": "image/x-xpixmap", - "xwd": "image/x-xwindowdump" - }; - - return function (path) { - var extension = utils.fileExtension(path); - return map[extension] || '*'; - }; - })(), + extensionMimeTypeMap: { + "bmp": "image/bmp", + "cmx": "image/x-cmx", + "cod": "image/cis-cod", + "gif": "image/gif", + "ico": "image/x-icon", + "ief": "image/ief", + "jfif": "image/pipeg", + "jpe": "image/jpeg", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "png": "image/png", + "pbm": "image/x-portable-bitmap", + "pgm": "image/x-portable-graymap", + "pnm": "image/x-portable-anymap", + "ppm": "image/x-portable-pixmap", + "ras": "image/x-cmu-raster", + "rgb": "image/x-rgb", + "svg": "image/svg+xml", + "tif": "image/tiff", + "tiff": "image/tiff", + "xbm": "image/x-xbitmap", + "xpm": "image/x-xpixmap", + "xwd": "image/x-xwindowdump" + }, + + fileMimeType: function (path) { + utils.extensionToMimeType(utils.fileExtension(path)); + }, + + extensionToMimeType: function(extension) { + return utils.extensionMimeTypeMap[extension] || '*'; + }, isRelativeUrl: function(url) { var firstChar = url.slice(0, 1); @@ -264,9 +271,12 @@ value = options.skipToType[key] ? decodeURI(val[1]) : utils.toType(decodeURI(val[1])); if (key) { + if (key.substr(-2, 2) === '[]') { + key = key.slice(0, -2); + } if (!hash[key]) { hash[key] = value; - } else { + } else { if (!$.isArray(hash[key])) { hash[key] = [hash[key]]; } @@ -287,24 +297,6 @@ return a; }, - getQueryParams: function() { - var search = window.location.search.slice(1), - data = {}; - - search = search.split('&'); - for(var x=0,numParams=search.length,temp;x uploadSize * 1024) { - fs.unlink(userPhoto.path); - return next(new Error('[[error:file-too-big, ' + uploadSize + ']]')); - } - - var allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']; - if (allowedTypes.indexOf(userPhoto.type) === -1) { - fs.unlink(userPhoto.path); - return next(new Error('[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]')); - } - var extension = path.extname(userPhoto.name); - if (!extension) { - fs.unlink(userPhoto.path); - return next(new Error('[[error:invalid-image-extension]]')); - } - var updateUid = req.user ? req.user.uid : 0; var imageDimension = parseInt(meta.config.profileImageDimension, 10) || 128; + var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1; async.waterfall([ + function(next) { + next(userPhoto.size > uploadSize * 1024 ? new Error('[[error:file-too-big, ' + uploadSize + ']]') : null); + }, + function(next) { + next(!extension ? new Error('[[error:invalid-image-extension]]') : null); + }, + function(next) { + file.isFileTypeAllowed(userPhoto.path, ['png', 'jpeg', 'jpg', 'gif'], next); + }, function(next) { image.resizeImage(userPhoto.path, extension, imageDimension, imageDimension, next); }, function(next) { - if (parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1) { + if (convertToPNG) { image.convertImageToPng(userPhoto.path, extension, next); } else { next(); @@ -412,7 +404,7 @@ accountsController.uploadPicture = function (req, res, next) { user.getUidByUserslug(req.params.userslug, next); }, function(uid, next) { - if(parseInt(updateUid, 10) === parseInt(uid, 10)) { + if (parseInt(updateUid, 10) === parseInt(uid, 10)) { return next(); } @@ -450,7 +442,6 @@ accountsController.uploadPicture = function (req, res, next) { return plugins.fireHook('filter:uploadImage', {image: userPhoto, uid: updateUid}, done); } - var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1; var filename = updateUid + '-profileimg' + (convertToPNG ? '.png' : extension); user.getUserField(updateUid, 'uploadedpicture', function (err, oldpicture) { @@ -524,7 +515,7 @@ accountsController.getChats = function(req, res, next) { if (!toUid || parseInt(toUid, 10) === parseInt(req.user.uid, 10)) { return helpers.notFound(req, res); } - + async.parallel({ toUser: async.apply(user.getUserFields, toUid, ['uid', 'username']), messages: async.apply(messaging.getMessages, req.user.uid, toUid, 'recent', false), diff --git a/src/controllers/admin.js b/src/controllers/admin.js index f03b7fdb88..33e86a383a 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -163,14 +163,24 @@ adminController.tags.get = function(req, res, next) { }; adminController.flags.get = function(req, res, next) { - var uid = req.user ? parseInt(req.user.uid, 10) : 0; - posts.getFlags(uid, 0, 19, function(err, posts) { + function done(err, posts) { if (err) { return next(err); } - - res.render('admin/manage/flags', {posts: posts, next: 20}); - }); + res.render('admin/manage/flags', {posts: posts, next: end + 1, byUsername: byUsername}); + } + var uid = req.user ? parseInt(req.user.uid, 10) : 0; + var sortBy = req.query.sortBy || 'count'; + var byUsername = req.query.byUsername || ''; + var start = 0; + var end = 19; + + if (byUsername) { + posts.getUserFlags(byUsername, sortBy, uid, start, end, done); + } else { + var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'; + posts.getFlags(set, uid, start, end, done); + } }; adminController.database.get = function(req, res, next) { @@ -304,12 +314,21 @@ adminController.extend.widgets = function(req, res, next) { }); }; +adminController.extend.rewards = function(req, res, next) { + require('../rewards/admin').get(function(err, data) { + if (err) { + return next(err); + } + + res.render('admin/extend/rewards', data); + }); +}; adminController.groups.get = function(req, res, next) { groups.list({ expand: true, - showSystemGroups: true, - truncateUserList: true + truncateUserList: true, + isAdmin: true }, function(err, groups) { groups = groups.filter(function(group) { return group.name !== 'registered-users' && group.name !== 'guests'; diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 9ba3ad049c..81a98ed151 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -30,7 +30,7 @@ uploadsController.uploadFavicon = function(req, res, next) { var uploadedFile = req.files.files[0]; var allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; - if (validateUpload(res, req, next, uploadedFile, allowedTypes)) { + if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { file.saveFileToLocal('favicon.ico', 'files', uploadedFile.path, function(err, image) { fs.unlink(uploadedFile.path); if (err) { @@ -63,7 +63,7 @@ function validateUpload(req, res, next, uploadedFile, allowedTypes) { if (allowedTypes.indexOf(uploadedFile.type) === -1) { fs.unlink(uploadedFile.path); - next(new Error('[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]')); + res.json({error: '[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]'}); return false; } diff --git a/src/controllers/categories.js b/src/controllers/categories.js index 9ef288272c..d5cfda79c8 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -154,8 +154,8 @@ categoriesController.list = function(req, res, next) { if (err) { return next(err); } - // TODO: template should be called categories.tpl - res.render('home', data); + + res.render('categories', data); }); }; @@ -333,7 +333,12 @@ categoriesController.get = function(req, res, next) { res.locals.linkTags.push(rel); }); - res.render('category', data); + plugins.fireHook('filter:category.build', {req: req, res: res, templateData: data}, function(err, data) { + if (err) { + return next(err); + } + res.render('category', data.templateData); + }); }); }; diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 8c29f2b397..51200d0a40 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -12,7 +12,7 @@ groupsController.list = function(req, res, next) { groups.list({ truncateUserList: true, expand: true, - uid: req.user ? req.user.uid : 0 + uid: req.user ? parseInt(req.user.uid, 10) : 0 }, function(err, groups) { if (err) { return next(err); @@ -27,17 +27,40 @@ groupsController.list = function(req, res, next) { groupsController.details = function(req, res, next) { var uid = req.user ? parseInt(req.user.uid, 10) : 0; - groups.existsBySlug(req.params.slug, function(err, exists) { - if (exists) { + async.waterfall([ + async.apply(groups.exists, res.locals.groupName), + function(exists, next) { + if (!exists) { return next(undefined, null); } + + // Ensure the group isn't hidden either + groups.isHidden(res.locals.groupName, next); + }, + function(hidden, next) { + if (hidden === null) { return next(undefined, false); } // Group didn't exist, not ok + + if (!hidden) { + next(null, true); + } else { + // If not, only members are granted access + async.parallel([ + async.apply(groups.isMember, uid, res.locals.groupName), + async.apply(groups.isInvited, uid, res.locals.groupName) + ], function(err, checks) { + next(err, checks[0] || checks[1]); + }); + } + } + ], function(err, ok) { + if (ok) { async.parallel({ group: function(next) { - groups.getByGroupslug(req.params.slug, { + groups.get(res.locals.groupName, { expand: true, uid: uid }, next); }, posts: function(next) { - groups.getLatestMemberPosts(req.params.slug, 10, uid, next); + groups.getLatestMemberPosts(res.locals.groupName, 10, uid, next); } }, function(err, results) { if (err) { diff --git a/src/controllers/index.js b/src/controllers/index.js index 805ba52d20..d8ddf81735 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -16,6 +16,7 @@ var async = require('async'), helpers = require('./helpers'); var Controllers = { + posts: require('./posts'), topics: require('./topics'), categories: require('./categories'), tags: require('./tags'), @@ -30,13 +31,15 @@ var Controllers = { Controllers.home = function(req, res, next) { - var route = meta.config.homePageRoute || 'home'; - if (route === 'home') { + var route = meta.config.homePageRoute || 'categories'; + if (route === 'categories') { return Controllers.categories.list(req, res, next); } else if (route === 'recent') { Controllers.categories.recent(req, res, next); } else if (route === 'popular') { Controllers.categories.popular(req, res, next); + } else { + next(); } }; @@ -68,6 +71,7 @@ Controllers.login = function(req, res, next) { data.showResetLink = emailersPresent; data.allowLocalLogin = parseInt(meta.config.allowLocalLogin, 10) === 1; data.allowRegistration = parseInt(meta.config.allowRegistration, 10) === 1; + data.allowLoginWith = '[[login:' + (meta.config.allowLoginWith || 'username-email') + ']]'; data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:login]]'}]); data.error = req.flash('error')[0]; diff --git a/src/controllers/posts.js b/src/controllers/posts.js new file mode 100644 index 0000000000..e5a32e9791 --- /dev/null +++ b/src/controllers/posts.js @@ -0,0 +1,36 @@ +"use strict"; + +var async = require('async'), + + posts = require('../posts'), + privileges = require('../privileges'), + helpers = require('./helpers'), + postsController = {}; + +postsController.getPost = function(req, res, next) { + var uid = req.user ? parseInt(req.user.uid) : 0; + async.parallel({ + canRead: function(next) { + privileges.posts.can('read', req.params.pid, uid, next); + }, + postData: function(next) { + posts.getPostData(req.params.pid, next); + } + }, function(err, results) { + if (err) { + return next(err); + } + if (!results.postData) { + return helpers.notFound(req, res); + } + if (!results.canRead) { + return helpers.notAllowed(req, res); + } + + res.json(results.postData); + }); +}; + + + +module.exports = postsController; diff --git a/src/controllers/search.js b/src/controllers/search.js index 2020612094..7ed16d8bb8 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -31,7 +31,8 @@ searchController.search = function(req, res, next) { users: [], tags: [], categories: categories, - breadcrumbs: breadcrumbs + breadcrumbs: breadcrumbs, + expandSearch: true }; plugins.fireHook('filter:search.build', {data: {}, results: results}, function(err, data) { if (err) { @@ -75,6 +76,7 @@ searchController.search = function(req, res, next) { results.showAsTopics = req.query.showAs === 'topics'; results.breadcrumbs = breadcrumbs; results.categories = categories; + results.expandSearch = false; plugins.fireHook('filter:search.build', {data: data, results: results}, function(err, data) { if (err) { diff --git a/src/controllers/templates.js b/src/controllers/templates.js index 0d686a1455..dbf0be4870 100644 --- a/src/controllers/templates.js +++ b/src/controllers/templates.js @@ -24,7 +24,7 @@ templatesController.getTemplatesListing = function(req, res, next) { readConfigFile(next); }, function(config, next) { - config.custom_mapping['^/?$'] = meta.config.homePageRoute || 'home'; + config.custom_mapping['^/?$'] = meta.config.homePageRoute || 'categories'; plugins.fireHook('filter:templates.get_config', config, next); } diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 498a3d81db..1d756b4189 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -266,7 +266,13 @@ topicsController.get = function(req, res, next) { }); topics.increaseViewCount(tid); - res.render('topic', data); + + plugins.fireHook('filter:topic.build', {req: req, res: res, templateData: data}, function(err, data) { + if (err) { + return next(err); + } + res.render('topic', data.templateData); + }); }); }; diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index b7e590b6bf..79ae6fae19 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -5,8 +5,10 @@ var uploadsController = {}, fs = require('fs'), path = require('path'), async = require('async'), + validator = require('validator'), meta = require('../meta'), + file = require('../file'), plugins = require('../plugins'), utils = require('../../public/src/utils'), image = require('../image'); @@ -42,12 +44,18 @@ uploadsController.upload = function(req, res, filesIterator, next) { }; uploadsController.uploadPost = function(req, res, next) { - uploadsController.upload(req, res, function(file, next) { - if (file.type.match(/image./)) { - uploadImage(req.user.uid, file, next); - } else { - uploadFile(req.user.uid, file, next); - } + uploadsController.upload(req, res, function(uploadedFile, next) { + file.isFileTypeAllowed(uploadedFile.path, file.allowedExtensions(), function(err) { + if (err) { + return next(err); + } + + if (uploadedFile.type.match(/image./)) { + uploadImage(req.user.uid, uploadedFile, next); + } else { + uploadFile(req.user.uid, uploadedFile, next); + } + }); }, next); }; @@ -57,18 +65,24 @@ uploadsController.uploadThumb = function(req, res, next) { return next(new Error('[[error:topic-thumbnails-are-disabled]]')); } - uploadsController.upload(req, res, function(file, next) { - if(file.type.match(/image./)) { - var size = meta.config.topicThumbSize || 120; - image.resizeImage(file.path, path.extname(file.name), size, size, function(err) { - if (err) { - return next(err); - } - uploadImage(req.user.uid, file, next); - }); - } else { - next(new Error('[[error:invalid-file]]')); - } + uploadsController.upload(req, res, function(uploadedFile, next) { + file.isFileTypeAllowed(uploadedFile.path, file.allowedExtensions(), function(err) { + if (err) { + return next(err); + } + + if (uploadedFile.type.match(/image./)) { + var size = meta.config.topicThumbSize || 120; + image.resizeImage(uploadedFile.path, path.extname(uploadedFile.name), size, size, function(err) { + if (err) { + return next(err); + } + uploadImage(req.user.uid, uploadedFile, next); + }); + } else { + next(new Error('[[error:invalid-file]]')); + } + }); }, next); }; @@ -88,32 +102,34 @@ function uploadImage(uid, image, callback) { } } -function uploadFile(uid, file, callback) { +function uploadFile(uid, uploadedFile, callback) { if (plugins.hasListeners('filter:uploadFile')) { - return plugins.fireHook('filter:uploadFile', {file: file, uid: uid}, callback); + return plugins.fireHook('filter:uploadFile', {file: uploadedFile, uid: uid}, callback); } if (parseInt(meta.config.allowFileUploads, 10) !== 1) { return callback(new Error('[[error:uploads-are-disabled]]')); } - if (!file) { + if (!uploadedFile) { return callback(new Error('[[error:invalid-file]]')); } - if (file.size > parseInt(meta.config.maximumFileSize, 10) * 1024) { + if (uploadedFile.size > parseInt(meta.config.maximumFileSize, 10) * 1024) { return callback(new Error('[[error:file-too-big, ' + meta.config.maximumFileSize + ']]')); } + + var filename = uploadedFile.name || 'upload'; - var filename = 'upload-' + utils.generateUUID() + path.extname(file.name); - require('../file').saveFileToLocal(filename, 'files', file.path, function(err, upload) { + filename = Date.now() + '-' + validator.escape(filename).substr(0, 255); + file.saveFileToLocal(filename, 'files', uploadedFile.path, function(err, upload) { if (err) { return callback(err); } callback(null, { url: upload.url, - name: file.name + name: uploadedFile.name }); }); } diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index f40a756600..da1d2483d9 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -200,13 +200,25 @@ module.exports = function(db, module) { }; module.deleteObjectField = function(key, field, callback) { + module.deleteObjectFields(key, [field], callback); + }; + + module.deleteObjectFields = function(key, fields, callback) { callback = callback || helpers.noop; - if (!key || !field) { + if (!key || !Array.isArray(fields) || !fields.length) { + return callback(); + } + fields = fields.filter(Boolean); + if (!fields.length) { return callback(); } + var data = {}; - field = helpers.fieldToString(field); - data[field] = ''; + fields.forEach(function(field) { + field = helpers.fieldToString(field); + data[field] = ''; + }); + db.collection('objects').update({_key: key}, {$unset : data}, function(err, res) { callback(err); }); diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 475360c2ca..8509475bc4 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -138,7 +138,7 @@ module.exports = function(db, module) { }; module.pexpire = function(key, ms, callback) { - module.expireAt(key, Date.now() + parseInt(ms, 10), callback); + module.pexpireAt(key, Date.now() + parseInt(ms, 10), callback); }; module.pexpireAt = function(key, timestamp, callback) { diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index 92ae7017c0..ce79d0bc6f 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -94,6 +94,12 @@ module.exports = function(redisClient, module) { }); }; + module.deleteObjectFields = function(key, fields, callback) { + helpers.multiKeyValues(redisClient, 'hdel', key, fields, function(err, results) { + callback(err); + }); + }; + module.incrObjectField = function(key, field, callback) { redisClient.hincrby(key, field, 1, callback); }; diff --git a/src/favourites.js b/src/favourites.js index fec00fd9cc..0c9c81dc46 100644 --- a/src/favourites.js +++ b/src/favourites.js @@ -174,6 +174,7 @@ var async = require('async'), plugins.fireHook('action:post.' + hook, { pid: pid, uid: uid, + owner: results.owner, current: current }); diff --git a/src/file.js b/src/file.js index 9433dfb40b..a3b594f326 100644 --- a/src/file.js +++ b/src/file.js @@ -3,7 +3,12 @@ var fs = require('fs'), nconf = require('nconf'), path = require('path'), - winston = require('winston'); + winston = require('winston'), + mmmagic = require('mmmagic'), + Magic = mmmagic.Magic, + mime = require('mime'), + + meta= require('./meta'); var file = {}; @@ -11,7 +16,7 @@ file.saveFileToLocal = function(filename, folder, tempPath, callback) { var uploadPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), folder, filename); - winston.info('Saving file '+ filename +' to : ' + uploadPath); + winston.verbose('Saving file '+ filename +' to : ' + uploadPath); var is = fs.createReadStream(tempPath); var os = fs.createWriteStream(uploadPath); @@ -30,4 +35,38 @@ file.saveFileToLocal = function(filename, folder, tempPath, callback) { is.pipe(os); }; +file.isFileTypeAllowed = function(path, allowedExtensions, callback) { + if (!Array.isArray(allowedExtensions) || !allowedExtensions.length) { + return callback(); + } + + allowedExtensions = allowedExtensions.filter(Boolean).map(function(extension) { + return extension.trim(); + }); + + var magic = new Magic(mmmagic.MAGIC_MIME_TYPE); + magic.detectFile(path, function(err, mimeType) { + if (err) { + return callback(err); + } + + var uploadedFileExtension = mime.extension(mimeType); + + if (allowedExtensions.indexOf(uploadedFileExtension) === -1) { + return callback(new Error('[[error:invalid-file-type, ' + allowedExtensions.join('-') + ']]')); + } + + callback(); + }); +}; + +file.allowedExtensions = function() { + var allowedExtensions = (meta.config.allowedFileExtensions || '').trim(); + if (!allowedExtensions) { + return []; + } + allowedExtensions = allowedExtensions.split(','); + return allowedExtensions; +}; + module.exports = file; \ No newline at end of file diff --git a/src/groups.js b/src/groups.js index f82e452b96..54ff60a195 100644 --- a/src/groups.js +++ b/src/groups.js @@ -31,7 +31,7 @@ var async = require('async'), if (!group) { return false; } - if (group.deleted || (group.hidden && !group.system) || (!options.showSystemGroups && group.system)) { + if (group.deleted || (group.hidden && !(group.system || group.isMember || options.isAdmin || group.isInvited)) || (!options.showSystemGroups && group.system)) { return false; } else if (options.removeEphemeralGroups && ephemeralGroups.indexOf(group.name) !== -1) { return false; @@ -43,20 +43,14 @@ var async = require('async'), return groups; } }, - getEphemeralGroup: function(groupName, options, callback) { - Groups.exists(groupName, function(err, exists) { - if (!err && exists) { - Groups.get.apply(null, arguments); - } else { - callback(null, { - name: groupName, - description: '', - deleted: '0', - hidden: '0', - system: '1' - }); - } - }); + getEphemeralGroup: function(groupName) { + return { + name: groupName, + description: '', + deleted: '0', + hidden: '0', + system: '1' + }; }, removeEphemeralGroups: function(groups) { var x = groups.length; @@ -67,12 +61,8 @@ var async = require('async'), } return groups; - }/*, - fixImageUrl: function(url) { - if (url) { - return url.indexOf('http') === -1 ? nconf.get('relative_path') + url : url; - } - }*/ + }, + isPrivilegeGroup: /^cid:\d+:privileges:[\w:]+$/ }; Groups.list = function(options, callback) { @@ -82,10 +72,17 @@ var async = require('async'), } groupNames = groupNames.concat(ephemeralGroups); - async.map(groupNames, function (groupName, next) { - Groups.get(groupName, options, next); - }, function (err, groups) { - callback(err, internals.filterGroups(groups, options)); + async.parallel({ + groups: async.apply(async.map, groupNames, function (groupName, next) { + Groups.get(groupName, options, next); + }), + isAdmin: function(next) { + if (!options.uid || parseInt(options.uid, 10) === 0) { return next(null, false); } + user.isAdministrator(parseInt(options.uid, 10), next); + } + }, function (err, data) { + options.isAdmin = options.isAdmin || data.isAdmin; + callback(err, internals.filterGroups(data.groups, options)); }); }); }; @@ -103,7 +100,7 @@ var async = require('async'), if (ephemeralGroups.indexOf(groupName) === -1) { db.getObject('group:' + groupName, next); } else { - internals.getEphemeralGroup(groupName, options, next); + next(null, internals.getEphemeralGroup(groupName)); } }, users: function (next) { @@ -182,6 +179,7 @@ var async = require('async'), db.isSetMember('group:' + groupName + ':pending', options.uid, next); }, + isInvited: async.apply(Groups.isInvited, options.uid, groupName), isOwner: function(next) { // Retrieve group ownership state, if uid is passed in if (!options.uid) { @@ -210,28 +208,35 @@ var async = require('async'), results.base['cover:position'] = '50% 50%'; } - results.base.name = validator.escape(results.base.name); - results.base.description = validator.escape(results.base.description); - results.base.userTitle = validator.escape(results.base.userTitle); - results.base.createtimeISO = utils.toISOString(results.base.createtime); - results.base.members = results.users.filter(Boolean); - results.base.pending = results.pending.filter(Boolean); - results.base.count = numUsers || results.base.members.length; - results.base.memberCount = numUsers || results.base.members.length; - results.base.deleted = !!parseInt(results.base.deleted, 10); - results.base.hidden = !!parseInt(results.base.hidden, 10); - results.base.system = !!parseInt(results.base.system, 10); - results.base.private = results.base.private ? !!parseInt(results.base.private, 10) : true; - results.base.deletable = !results.base.system; - results.base.truncated = truncated; - results.base.isMember = results.isMember; - results.base.isPending = results.isPending; - results.base.isOwner = results.isOwner; - - - plugins.fireHook('filter:group.get', {group: results.base}, function(err, data) { - callback(err, data ? data.group : null); - }); + plugins.fireHook('filter:parse.raw', results.base.description, function(err, descriptionParsed) { + if (err) { + return callback(err); + } + results.base.name = validator.escape(results.base.name); + results.base.description = validator.escape(results.base.description); + results.base.descriptionParsed = descriptionParsed; + results.base.userTitle = validator.escape(results.base.userTitle); + results.base.createtimeISO = utils.toISOString(results.base.createtime); + results.base.members = results.users.filter(Boolean); + results.base.pending = results.pending.filter(Boolean); + results.base.count = numUsers || results.base.members.length; + results.base.memberCount = numUsers || results.base.members.length; + results.base.deleted = !!parseInt(results.base.deleted, 10); + results.base.hidden = !!parseInt(results.base.hidden, 10); + results.base.system = !!parseInt(results.base.system, 10); + results.base.private = results.base.private ? !!parseInt(results.base.private, 10) : true; + results.base.deletable = !results.base.system; + results.base.truncated = truncated; + results.base.isMember = results.isMember; + results.base.isPending = results.isPending; + results.base.isInvited = results.isInvited; + results.base.isOwner = results.isOwner; + + + plugins.fireHook('filter:group.get', {group: results.base}, function(err, data) { + callback(err, data ? data.group : null); + }); + }); }); }; @@ -272,6 +277,17 @@ var async = require('async'), }); }; + Groups.isHidden = function(groupName, callback) { + Groups.getGroupFields(groupName, ['hidden'], function(err, values) { + if (err) { + winston.warn('[groups.isHidden] Could not determine group hidden state (group: ' + groupName + ')'); + return callback(null, true); // Default true + } + + callback(null, parseInt(values.hidden, 10)); + }); + }; + Groups.getMembers = function(groupName, start, end, callback) { db.getSortedSetRevRange('group:' + groupName + ':members', start, end, callback); }; @@ -394,12 +410,22 @@ var async = require('async'), }); }; + Groups.isInvited = function(uid, groupName, callback) { + if (!uid) { return callback(null, false); } + db.isSetMember('group:' + groupName + ':invited', uid, callback); + }; + Groups.exists = function(name, callback) { if (Array.isArray(name)) { var slugs = name.map(function(groupName) { return utils.slugify(groupName); }); async.parallel([ + function(next) { + callback(null, slugs.map(function(slug) { + return ephemeralGroups.indexOf(slug) !== -1; + })); + }, async.apply(db.isObjectFields, 'groupslug:groupname', slugs), async.apply(db.isSortedSetMembers, 'groups:createtime', name) ], function(err, results) { @@ -407,17 +433,20 @@ var async = require('async'), return callback(err); } - callback(null, results.map(function(pair) { - return pair[0] || pair[1]; + callback(null, results.map(function(result) { + return result[0] || result[1] || result[2]; })); }); } else { var slug = utils.slugify(name); async.parallel([ + function(next) { + next(null, ephemeralGroups.indexOf(slug) !== -1); + }, async.apply(db.isObjectField, 'groupslug:groupname', slug), async.apply(db.isSortedSetMember, 'groups:createtime', name) ], function(err, results) { - callback(err, !err ? (results[0] || results[1]) : null); + callback(err, !err ? (results[0] || results[1] || results[2]) : null); }); } }; @@ -431,7 +460,7 @@ var async = require('async'), return callback(new Error('[[error:group-name-too-short]]')); } - if (data.name === 'administrators' || data.name === 'registered-users') { + if (data.name === 'administrators' || data.name === 'registered-users' || internals.isPrivilegeGroup.test(data.name)) { var system = true; } @@ -499,7 +528,7 @@ var async = require('async'), description: values.description || '', icon: values.icon || '', labelColor: values.labelColor || '#000000', - hidden: values.hidden || '0', + hidden: values.hidden === true ? '1' : '0', 'private': values.private === false ? '0' : '1' }; @@ -585,6 +614,7 @@ var async = require('async'), async.apply(db.rename, 'group:' + oldName + ':members', 'group:' + newName + ':members'), async.apply(db.rename, 'group:' + oldName + ':owners', 'group:' + newName + ':owners'), async.apply(db.rename, 'group:' + oldName + ':pending', 'group:' + newName + ':pending'), + async.apply(db.rename, 'group:' + oldName + ':invited', 'group:' + newName + ':invited'), async.apply(renameGroupMember, 'groups:createtime', oldName, newName), function(next) { plugins.fireHook('action:group.rename', { @@ -629,6 +659,7 @@ var async = require('async'), async.apply(db.sortedSetRemove, 'groups:createtime', groupName), async.apply(db.delete, 'group:' + groupName + ':members'), async.apply(db.delete, 'group:' + groupName + ':pending'), + async.apply(db.delete, 'group:' + groupName + ':invited'), async.apply(db.delete, 'group:' + groupName + ':owners'), async.apply(db.deleteObjectField, 'groupslug:groupname', utils.slugify(groupName)), function(next) { @@ -725,18 +756,41 @@ var async = require('async'), Groups.acceptMembership = function(groupName, uid, callback) { // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! async.waterfall([ - function(next) { - db.setRemove('group:' + groupName + ':pending', uid, next); - }, - function(next) { - Groups.join(groupName, uid, next); - } + async.apply(db.setRemove, 'group:' + groupName + ':pending', uid), + async.apply(db.setRemove, 'group:' + groupName + ':invited', uid), + async.apply(Groups.join, groupName, uid) ], callback); }; Groups.rejectMembership = function(groupName, uid, callback) { // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! - db.setRemove('group:' + groupName + ':pending', uid, callback); + async.parallel([ + async.apply(db.setRemove, 'group:' + groupName + ':pending', uid), + async.apply(db.setRemove, 'group:' + groupName + ':invited', uid) + ], callback); + }; + + Groups.invite = function(groupName, uid, callback) { + async.parallel({ + exists: async.apply(Groups.exists, groupName), + isMember: async.apply(Groups.isMember, uid, groupName) + }, function(err, checks) { + if (!checks.exists) { + return callback(new Error('[[error:no-group]]')); + } else if (checks.isMember) { + return callback(new Error('[[error:group-already-member]]')); + } + + if (parseInt(uid, 10) > 0) { + db.setAdd('group:' + groupName + ':invited', uid, callback); + plugins.fireHook('action:group.inviteMember', { + groupName: groupName, + uid: uid + }); + } else { + callback(new Error('[[error:not-logged-in]]')); + } + }); }; Groups.leave = function(groupName, uid, callback) { @@ -790,12 +844,9 @@ var async = require('async'), }); }; - Groups.getLatestMemberPosts = function(groupSlug, max, uid, callback) { + Groups.getLatestMemberPosts = function(groupName, max, uid, callback) { async.waterfall([ - async.apply(Groups.getGroupNameByGroupSlug, groupSlug), - function(groupName, next) { - Groups.getMembers(groupName, 0, -1, next); - }, + async.apply(Groups.getMembers, groupName, 0, -1), function(uids, next) { if (!Array.isArray(uids) || !uids.length) { return callback(null, []); @@ -814,40 +865,58 @@ var async = require('async'), ], callback); }; + Groups.getGroupsData = function(groupNames, callback) { + if (!Array.isArray(groupNames) || !groupNames.length) { + return callback(null, []); + } + var keys = groupNames.map(function(groupName) { + return 'group:' + groupName; + }); + + db.getObjects(keys, function(err, groupData) { + if (err) { + return callback(err); + } + groupData = groupData.map(function(group) { + if (group) { + group.labelColor = group.labelColor || '#000000'; + group.createtimeISO = utils.toISOString(group.createtime); + group.hidden = parseInt(group.hidden, 10) === 1; + + if (!group['cover:url']) { + group['cover:url'] = nconf.get('relative_path') + '/images/cover-default.png'; + group['cover:position'] = '50% 50%'; + } + } + return group; + }); + + plugins.fireHook('filter:groups.get', {groups: groupData}, function(err, data) { + callback(err, data ? data.groups : null); + }); + }); + }; + Groups.getUserGroups = function(uids, callback) { db.getSortedSetRevRange('groups:createtime', 0, -1, function(err, groupNames) { if (err) { return callback(err); } - var groupKeys = groupNames.filter(function(groupName) { + groupNames = groupNames.filter(function(groupName) { return groupName !== 'registered-users' && groupName.indexOf(':privileges:') === -1; - }).map(function(groupName) { - return 'group:' + groupName; }); - db.getObjects(groupKeys, function(err, groupData) { + Groups.getGroupsData(groupNames, function(err, groupData) { if (err) { return callback(err); } groupData = groupData.filter(function(group) { - return parseInt(group.hidden, 10) !== 1 && !!group.userTitle; - }).map(function(group) { - group.createtimeISO = utils.toISOString(group.createtime); - return group; + return group && parseInt(group.hidden, 10) !== 1 && !!group.userTitle; }); - var groupSets = groupData.map(function(group) { - group.labelColor = group.labelColor || '#000000'; - group.createtimeISO = utils.toISOString(group.createtime); - - if (!group['cover:url']) { - group['cover:url'] = nconf.get('relative_path') + '/images/cover-default.png'; - group['cover:position'] = '50% 50%'; - } - return 'group:' + group.name + ':members'; }); diff --git a/src/install.js b/src/install.js index 45d44d75ad..d3df90b267 100644 --- a/src/install.js +++ b/src/install.js @@ -422,10 +422,14 @@ function enableDefaultPlugins(next) { 'nodebb-plugin-markdown', 'nodebb-plugin-mentions', 'nodebb-widget-essentials', + 'nodebb-rewards-essentials', 'nodebb-plugin-soundpack-default' ]; var db = require('./database'); - db.setAdd('plugins:active', defaultEnabled, next); + var order = defaultEnabled.map(function(plugin, index) { + return index; + }); + db.sortedSetAdd('plugins:active', order, defaultEnabled, next); } function setCopyrightWidget(next) { diff --git a/src/messaging.js b/src/messaging.js index 339ec7798c..4a367fbd38 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -10,7 +10,6 @@ var db = require('./database'), utils = require('../public/src/utils'), notifications = require('./notifications'), userNotifications = require('./user/notifications'), - websockets = require('./socket.io'), emailer = require('./emailer'); (function(Messaging) { @@ -257,7 +256,7 @@ var db = require('./database'), results.users.forEach(function(user, index) { if (user) { user.unread = results.unread[index]; - user.status = websockets.isUserOnline(user.uid) ? user.status : 'offline'; + user.status = require('./socket.io').isUserOnline(user.uid) ? user.status : 'offline'; } }); @@ -320,33 +319,34 @@ var db = require('./database'), }; function sendNotifications(fromuid, touid, messageObj, callback) { - if (!websockets.isUserOnline(touid)) { - notifications.create({ - bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', - bodyLong: messageObj.content, - path: nconf.get('relative_path') + '/chats/' + utils.slugify(messageObj.fromUser.username), - nid: 'chat_' + fromuid + '_' + touid, - from: fromuid - }, function(err, notification) { - if (!err && notification) { - notifications.push(notification, [touid], callback); - } - }); - - user.getSettings(messageObj.toUser.uid, function(err, settings) { - if (settings.sendChatNotifications && !parseInt(meta.config.disableEmailSubscriptions, 10)) { - emailer.send('notif_chat', touid, { - subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]', - username: messageObj.toUser.username, - summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', - message: messageObj, - site_title: meta.config.title || 'NodeBB', - url: nconf.get('url'), - fromUserslug: utils.slugify(messageObj.fromUser.username) - }); - } - }); + if (require('./socket.io').isUserOnline(touid)) { + return callback(); } + + notifications.create({ + bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', + bodyLong: messageObj.content, + nid: 'chat_' + fromuid + '_' + touid, + from: fromuid + }, function(err, notification) { + if (!err && notification) { + notifications.push(notification, [touid], callback); + } + }); + + user.getSettings(messageObj.toUser.uid, function(err, settings) { + if (settings.sendChatNotifications && !parseInt(meta.config.disableEmailSubscriptions, 10)) { + emailer.send('notif_chat', touid, { + subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]', + username: messageObj.toUser.username, + summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', + message: messageObj, + site_title: meta.config.title || 'NodeBB', + url: nconf.get('url'), + fromUserslug: utils.slugify(messageObj.fromUser.username) + }); + } + }); } }(exports)); diff --git a/src/meta.js b/src/meta.js index 0e07681223..b58ac1a9b2 100644 --- a/src/meta.js +++ b/src/meta.js @@ -8,7 +8,6 @@ var async = require('async'), user = require('./user'), groups = require('./groups'), - plugins = require('./plugins'), emitter = require('./emitter'), pubsub = require('./pubsub'), auth = require('./routes/authentication'); @@ -49,6 +48,8 @@ var async = require('async'), function reload(callback) { callback = callback || function() {}; + + var plugins = require('./plugins'); async.series([ async.apply(plugins.clearRequireCache), async.apply(plugins.reload), diff --git a/src/meta/css.js b/src/meta/css.js index 9c00ba8633..e0f0412e1c 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -108,7 +108,7 @@ module.exports = function(Meta) { fs.exists(cachePath, function(exists) { if (exists) { if (nconf.get('isPrimary') === 'true') { - winston.verbose('[meta/css] (Experimental) Reading stylesheets from file'); + winston.verbose('[meta/css] Reading stylesheets from file'); async.map([cachePath, acpCachePath], fs.readFile, function(err, files) { Meta.css.cache = files[0]; Meta.css.acpCache = files[1]; @@ -120,7 +120,7 @@ module.exports = function(Meta) { callback(); } } else { - winston.warn('[meta/css] (Experimental) No stylesheets found on disk, re-minifying'); + winston.warn('[meta/css] No stylesheets found on disk, re-minifying'); Meta.css.minify.apply(Meta.css, arguments); } }); diff --git a/src/meta/js.js b/src/meta/js.js index a960d108ee..b8fa903c12 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -213,7 +213,7 @@ module.exports = function(Meta) { fs.exists(scriptPath, function(exists) { if (exists) { if (nconf.get('isPrimary') === 'true') { - winston.verbose('[meta/js] (Experimental) Reading client-side scripts from file'); + winston.verbose('[meta/js] Reading client-side scripts from file'); async.map([scriptPath, mapPath], fs.readFile, function(err, files) { Meta.js.cache = files[0]; Meta.js.map = files[1]; @@ -225,7 +225,7 @@ module.exports = function(Meta) { callback(); } } else { - winston.warn('[meta/js] (Experimental) No script file found on disk, re-minifying'); + winston.warn('[meta/js] No script file found on disk, re-minifying'); Meta.js.minify.apply(Meta.js, arguments); } }); diff --git a/src/meta/templates.js b/src/meta/templates.js index faf2208b46..af2f8664a6 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -15,7 +15,13 @@ var mkdirp = require('mkdirp'), Templates = {}; Templates.compile = function(callback) { - if (nconf.get('isPrimary') === 'false') { + var fromFile = nconf.get('from-file') || ''; + + if (nconf.get('isPrimary') === 'false' || fromFile.match('tpl')) { + if (fromFile.match('tpl')) { + winston.info('[minifier] Compiling templates skipped'); + } + emitter.emit('templates:compiled'); if (callback) { callback(); diff --git a/src/middleware/admin.js b/src/middleware/admin.js index a7ed1997fb..a23781fdef 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -40,13 +40,14 @@ middleware.buildHeader = function(req, res, next) { 'authentication': [] }; - user.getUserFields(uid, ['username', 'userslug', 'picture', 'email:confirmed'], function(err, userData) { + user.getUserFields(uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function(err, userData) { if (err) { return next(err); } userData.uid = uid; - + userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1; + async.parallel({ scripts: function(next) { plugins.fireHook('filter:admin.scripts.get', [], function(err, scripts) { @@ -72,16 +73,15 @@ middleware.buildHeader = function(req, res, next) { return next(err); } res.locals.config = results.config; + var data = { relative_path: nconf.get('relative_path'), configJSON: JSON.stringify(results.config), + user: userData, userJSON: JSON.stringify(userData), plugins: results.custom_header.plugins, authentication: results.custom_header.authentication, scripts: results.scripts, - userpicture: userData.picture, - username: userData.username, - userslug: userData.userslug, 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', env: process.env.NODE_ENV ? true : false }; diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 43e2a2ea86..349a227215 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -15,6 +15,7 @@ var app, meta = require('./../meta'), translator = require('./../../public/src/translator'), user = require('./../user'), + groups = require('./../groups'), db = require('./../database'), categories = require('./../categories'), topics = require('./../topics'), @@ -317,7 +318,7 @@ middleware.renderHeader = function(req, res, callback) { }, user: function(next) { if (uid) { - user.getUserFields(uid, ['username', 'userslug', 'picture', 'status', 'email:confirmed', 'banned'], next); + user.getUserFields(uid, ['username', 'userslug', 'email', 'picture', 'status', 'email:confirmed', 'banned'], next); } else { next(null, { username: '[[global:guest]]', @@ -342,7 +343,7 @@ middleware.renderHeader = function(req, res, callback) { results.user.isAdmin = results.isAdmin || false; results.user.uid = parseInt(results.user.uid, 10); results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1; - + templateValues.browserTitle = results.title; templateValues.isAdmin = results.user.isAdmin; templateValues.user = results.user; @@ -518,6 +519,17 @@ middleware.publicTagListing = function(req, res, next) { } }; +middleware.exposeGroupName = function(req, res, next) { + if (!req.params.hasOwnProperty('slug')) { return next(); } + + groups.getGroupNameByGroupSlug(req.params.slug, function(err, groupName) { + if (err) { return next(err); } + + res.locals.groupName = groupName; + next(); + }); +}; + module.exports = function(webserver) { app = webserver; middleware.admin = require('./admin')(webserver); diff --git a/src/notifications.js b/src/notifications.js index dc3aae7ea2..5beed15f9c 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -55,11 +55,12 @@ var async = require('async'), } if (notification.from && !notification.image) { - User.getUserField(notification.from, 'picture', function(err, picture) { + User.getUserFields(notification.from, ['username', 'userslug', 'picture'], function(err, userData) { if (err) { return next(err); } - notification.image = picture; + notification.image = userData.picture; + notification.user = userData; next(null, notification); }); return; @@ -223,6 +224,22 @@ var async = require('async'), Notifications.markReadMultiple([nid], uid, callback); }; + Notifications.markUnread = function(nid, uid, callback) { + callback = callback || function() {}; + if (!parseInt(uid, 10) || !nid) { + return callback(); + } + + db.getObjectField(nid, 'datetime', function(err, datetime) { + datetime = datetime || Date.now(); + + async.parallel([ + async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid), + async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', datetime, nid) + ], callback); + }); + }; + Notifications.markReadMultiple = function(nids, uid, callback) { callback = callback || function() {}; if (!Array.isArray(nids) || !nids.length) { diff --git a/src/plugins.js b/src/plugins.js index 730377c673..cb67084b28 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -14,13 +14,11 @@ var fs = require('fs'), translator = require('../public/src/translator'), utils = require('../public/src/utils'), hotswap = require('./hotswap'), - pkg = require('../package.json'), controllers = require('./controllers'), app, middleware; (function(Plugins) { - require('./plugins/install')(Plugins); require('./plugins/load')(Plugins); require('./plugins/hooks')(Plugins); @@ -92,13 +90,12 @@ var fs = require('fs'), Plugins.clientScripts.length = 0; Plugins.libraryPaths.length = 0; - // Read the list of activated plugins and require their libraries async.waterfall([ function(next) { - db.getSetMembers('plugins:active', next); + db.getSortedSetRange('plugins:active', 0, -1, next); }, function(plugins, next) { - if (!plugins || !Array.isArray(plugins)) { + if (!Array.isArray(plugins)) { return next(); } @@ -111,7 +108,7 @@ var fs = require('fs'), }); async.filter(plugins, fs.exists, function(plugins){ - async.each(plugins, Plugins.loadPlugin, next); + async.eachSeries(plugins, Plugins.loadPlugin, next); }); }, function(next) { @@ -170,8 +167,9 @@ var fs = require('fs'), }; Plugins.getAll = function(callback) { - var request = require('request'); - request((nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + pkg.version, function(err, res, body) { + var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins?version=' + require('../package.json').version; + + require('request')(url, function(err, res, body) { var plugins = []; try { @@ -187,8 +185,7 @@ var fs = require('fs'), plugins[i].installed = false; plugins[i].active = false; plugins[i].url = plugins[i].url ? plugins[i].url : plugins[i].repository ? plugins[i].repository.url : ''; - plugins[i].latest = getLatestVersion(plugins[i].versions); - // plugins[i].latest = plugins[i].latest; + plugins[i].latest = plugins[i].latest; pluginMap[plugins[i].name] = plugins[i]; } @@ -264,7 +261,10 @@ var fs = require('fs'), function(dirs, next) { dirs = dirs.filter(function(dir){ - return dir.startsWith('nodebb-plugin-') || dir.startsWith('nodebb-widget-') || dir.startsWith('nodebb-theme-') + return dir.startsWith('nodebb-plugin-') || + dir.startsWith('nodebb-widget-') || + dir.startsWith('nodebb-rewards-') || + dir.startsWith('nodebb-theme-'); }).map(function(dir){ return path.join(npmPluginPath, dir); }); diff --git a/src/plugins/install.js b/src/plugins/install.js index a95f16b3b1..6d6865301a 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -2,7 +2,6 @@ var winston = require('winston'), async = require('async'), - npm = require('npm'), path = require('path'), fs = require('fs'), nconf = require('nconf'), @@ -38,7 +37,16 @@ module.exports = function(Plugins) { }, function(_isActive, next) { isActive = _isActive; - db[isActive ? 'setRemove' : 'setAdd']('plugins:active', id, next); + if (isActive) { + db.sortedSetRemove('plugins:active', id, next); + } else { + db.sortedSetCard('plugins:active', function(err, count) { + if (err) { + return next(err); + } + db.sortedSetAdd('plugins:active', count, id, next); + }); + } }, function(next) { meta.reloadRequired = true; @@ -79,10 +87,10 @@ module.exports = function(Plugins) { next(); }, function(next) { - npm.load({}, next); + require('npm').load({}, next); }, function(res, next) { - npm.commands[type](installed ? id : [id + '@' + (version || 'latest')], next); + require('npm').commands[type](installed ? id : [id + '@' + (version || 'latest')], next); } ], function(err) { if (err) { @@ -102,10 +110,10 @@ module.exports = function(Plugins) { function upgrade(id, version, callback) { async.waterfall([ function(next) { - npm.load({}, next); + require('npm').load({}, next); }, function(res, next) { - npm.commands.install([id + '@' + (version || 'latest')], next); + require('npm').commands.install([id + '@' + (version || 'latest')], next); } ], callback); } @@ -119,6 +127,10 @@ module.exports = function(Plugins) { }; Plugins.isActive = function(id, callback) { - db.isSetMember('plugins:active', id, callback); + db.isSortedSetMember('plugins:active', id, callback); + }; + + Plugins.getActive = function(callback) { + db.getSortedSetRange('plugins:active', 0, -1, callback); }; }; \ No newline at end of file diff --git a/src/posts/category.js b/src/posts/category.js index 761738cb1f..68ae42474b 100644 --- a/src/posts/category.js +++ b/src/posts/category.js @@ -13,10 +13,7 @@ module.exports = function(Posts) { }, function(tid, next) { topics.getTopicField(tid, 'cid', next); - }, - function(cid, next) { - next(!cid ? new Error('[[error:invalid-cid]]') : null, cid); - } + } ], callback); }; diff --git a/src/posts/delete.js b/src/posts/delete.js index cc1a8c8b5b..bd130ade46 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -29,7 +29,7 @@ module.exports = function(Posts) { removeFromCategoryRecentPosts(pid, postData.tid, next); }, function(next) { - db.sortedSetRemove('posts:flagged', pid, next); + Posts.dismissFlag(pid, next); } ], function(err) { callback(err, postData); @@ -125,6 +125,9 @@ module.exports = function(Posts) { }, function(next) { db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next); + }, + function(next) { + Posts.dismissFlag(pid, next); } ], function(err) { if (err) { diff --git a/src/posts/flags.js b/src/posts/flags.js index c3b591b001..388b707d3d 100644 --- a/src/posts/flags.js +++ b/src/posts/flags.js @@ -3,22 +3,52 @@ 'use strict'; var async = require('async'), - db = require('../database'); + db = require('../database'), + user = require('../user'); module.exports = function(Posts) { - Posts.flag = function(pid, callback) { - Posts.exists(pid, function(err, exists) { - if (err || !exists) { + + Posts.flag = function(post, uid, callback) { + async.parallel({ + hasFlagged: async.apply(hasFlagged, post.pid, uid), + exists: async.apply(Posts.exists, post.pid) + }, function(err, results) { + if (err || !results.exists) { return callback(err || new Error('[[error:no-post]]')); } + if (results.hasFlagged) { + return callback(new Error('[[error:already-flagged]]')); + } + var now = Date.now(); + async.parallel([ function(next) { - db.sortedSetAdd('posts:flagged', Date.now(), pid, next); + db.sortedSetAdd('posts:flagged', now, post.pid, next); + }, + function(next) { + db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next); + }, + function(next) { + db.incrObjectField('post:' + post.pid, 'flags', next); + }, + function(next) { + db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next); }, function(next) { - db.incrObjectField('post:' + pid, 'flags', next); + if (parseInt(post.uid, 10)) { + db.sortedSetAdd('uid:' + post.uid + ':flag:pids', now, post.pid, next); + } else { + next(); + } + }, + function(next) { + if (parseInt(post.uid, 10)) { + db.setAdd('uid:' + post.uid + ':flagged_by', uid, next); + } else { + next(); + } } ], function(err, results) { callback(err); @@ -26,14 +56,31 @@ module.exports = function(Posts) { }); }; + function hasFlagged(pid, uid, callback) { + db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback); + } + Posts.dismissFlag = function(pid, callback) { async.parallel([ function(next) { - db.sortedSetRemove('posts:flagged', pid, next); + db.getObjectField('post:' + pid, 'uid', function(err, uid) { + if (err) { + return next(err); + } + + db.sortedSetsRemove([ + 'posts:flagged', + 'posts:flags:count', + 'uid:' + uid + ':flag:pids' + ], pid, next); + }); }, function(next) { db.deleteObjectField('post:' + pid, 'flags', next); - } + }, + function(next) { + db.delete('pid:' + pid + ':flag:uids', next); + } ], function(err, results) { callback(err); }); @@ -43,8 +90,8 @@ module.exports = function(Posts) { db.delete('posts:flagged', callback); }; - Posts.getFlags = function(uid, start, end, callback) { - db.getSortedSetRevRange('posts:flagged', start, end, function(err, pids) { + Posts.getFlags = function(set, uid, start, end, callback) { + db.getSortedSetRevRange(set, start, end, function(err, pids) { if (err) { return callback(err); } @@ -52,4 +99,29 @@ module.exports = function(Posts) { Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags']}, callback); }); }; + + Posts.getUserFlags = function(byUsername, sortBy, callerUID, start, end, callback) { + async.waterfall([ + function(next) { + user.getUidByUsername(byUsername, next); + }, + function(uid, next) { + if (!uid) { + return next(null, []); + } + db.getSortedSetRevRange('uid:' + uid + ':flag:pids', 0, -1, next); + }, + function(pids, next) { + Posts.getPostSummaryByPids(pids, callerUID, {stripTags: false, extraFields: ['flags']}, next); + }, + function(posts, next) { + if (sortBy === 'count') { + posts.sort(function(a, b) { + return b.flags - a.flags; + }); + } + next(null, posts.slice(start, end)); + } + ], callback); + }; }; diff --git a/src/posts/user.js b/src/posts/user.js index 555b60e46d..02b7816ed9 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -6,7 +6,6 @@ var async = require('async'), user = require('../user'), groups = require('../groups'), meta = require('../meta'), - websockets = require('../socket.io'), postTools = require('../postTools'), plugins = require('../plugins'); @@ -22,7 +21,7 @@ module.exports = function(Posts) { user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status'], next); }, online: function(next) { - websockets.isUsersOnline(uids, next); + require('../socket.io').isUsersOnline(uids, next); } }, function(err, results) { if (err) { diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 5ff70730d4..267c9e2c81 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -46,6 +46,9 @@ module.exports = function(privileges) { }; privileges.categories.can = function(privilege, cid, uid, callback) { + if (!cid) { + return callback(null, false); + } categories.getCategoryField(cid, 'disabled', function(err, disabled) { if (err) { return callback(err); diff --git a/src/privileges/posts.js b/src/privileges/posts.js index 7de1167726..7c304a1a2c 100644 --- a/src/privileges/posts.js +++ b/src/privileges/posts.js @@ -153,9 +153,10 @@ module.exports = function(privileges) { helpers.some([ function(next) { posts.getCidByPid(pid, function(err, cid) { - if (err) { - return next(err); + if (err || !cid) { + return next(err, false); } + user.isModerator(uid, cid, next); }); }, diff --git a/src/rewards/admin.js b/src/rewards/admin.js new file mode 100644 index 0000000000..6136e4f4b3 --- /dev/null +++ b/src/rewards/admin.js @@ -0,0 +1,131 @@ +"use strict"; + +var rewards = {}, + async = require('async'), + plugins = require('../plugins'), + db = require('../database'); + + +rewards.save = function(data, callback) { + function save(data, next) { + function commit(err, id) { + if (err) { + return callback(err); + } + + data.id = id; + + async.parallel([ + function(next) { + rewards.delete(data, next); + }, + function(next) { + db.setAdd('rewards:list', data.id, next); + }, + function(next) { + db.setObject('rewards:id:' + data.id, data, next); + }, + function(next) { + db.setObject('rewards:id:' + data.id + ':rewards', rewardsData, next); + } + ], next); + } + + if (!Object.keys(data.rewards).length) { + return next(); + } + + var rewardsData = data.rewards; + delete data.rewards; + + if (!parseInt(data.id, 10)) { + db.incrObjectField('global', 'rewards:id', commit); + } else { + commit(false, data.id); + } + } + + async.each(data, save, function(err) { + saveConditions(data, callback); + }); +}; + +rewards.delete = function(data, callback) { + async.parallel([ + function(next) { + db.setRemove('rewards:list', data.id, next); + }, + function(next) { + db.delete('rewards:id:' + data.id, next); + }, + function(next) { + db.delete('rewards:id:' + data.id + ':rewards', next); + } + ], callback); +}; + +rewards.get = function(callback) { + async.parallel({ + active: getActiveRewards, + conditions: function(next) { + plugins.fireHook('filter:rewards.conditions', [], next); + }, + conditionals: function(next) { + plugins.fireHook('filter:rewards.conditionals', [], next); + }, + rewards: function(next) { + plugins.fireHook('filter:rewards.rewards', [], next); + } + }, callback); +}; + +function saveConditions(data, callback) { + db.delete('conditions:active', function(err) { + if (err) { + return callback(err); + } + + var conditions = [], + rewardsPerCondition = {}; + + data.forEach(function(reward) { + conditions.push(reward.condition); + rewardsPerCondition[reward.condition] = rewardsPerCondition[reward.condition] || []; + rewardsPerCondition[reward.condition].push(reward.id); + }); + + db.setAdd('conditions:active', conditions, callback); + + async.each(Object.keys(rewardsPerCondition), function(condition, next) { + db.setAdd('condition:' + condition + ':rewards', rewardsPerCondition[condition]); + }, callback); + }); +} + +function getActiveRewards(callback) { + var activeRewards = []; + + function load(id, next) { + async.parallel({ + main: function(next) { + db.getObject('rewards:id:' + id, next); + }, + rewards: function(next) { + db.getObject('rewards:id:' + id + ':rewards', next); + } + }, function(err, data) { + data.main.rewards = data.rewards; + activeRewards.push(data.main); + + next(err); + }); + } + + db.getSetMembers('rewards:list', function(err, rewards) { + async.eachSeries(rewards, load, function(err) { + callback(err, activeRewards); + }); + }); +} + +module.exports = rewards; \ No newline at end of file diff --git a/src/rewards/index.js b/src/rewards/index.js new file mode 100644 index 0000000000..fb4fc9d0e8 --- /dev/null +++ b/src/rewards/index.js @@ -0,0 +1,122 @@ +"use strict"; + +var rewards = {}, + db = require('../database'), + plugins = require('../plugins'), + async = require('async'); + + +rewards.checkConditionAndRewardUser = function(uid, condition, method, callback) { + async.waterfall([ + function(next) { + isConditionActive(condition, function(err, isActive) { + if (!isActive) { + return back(err); + } + + next(err); + }); + }, + function(next) { + getIDsByCondition(condition, function(err, ids) { + next(err, ids); + }); + }, + function(ids, next) { + getRewardDataByIDs(ids, next); + }, + function(rewards, next) { + filterCompletedRewards(uid, rewards, function(err, filtered) { + if (!filtered || !filtered.length) { + return back(err); + } + + next(err, filtered); + }); + }, + function(rewards, next) { + async.filter(rewards, function(reward, next) { + if (!reward) { + return next(false); + } + + checkCondition(reward, method, next); + }, function(eligible) { + if (!eligible) { + return next(false); + } + + giveRewards(uid, eligible, next); + }); + } + ], back); + + + function back(err) { + if (typeof callback === 'function') { + callback(err); + } + } +}; + +function isConditionActive(condition, callback) { + db.isSetMember('conditions:active', condition, callback); +} + +function getIDsByCondition(condition, callback) { + db.getSetMembers('condition:' + condition + ':rewards', callback); +} + +function filterCompletedRewards(uid, rewards, callback) { + db.getSortedSetRangeByScoreWithScores('uid:' + uid + ':rewards', 0, -1, 1, Infinity, function(err, data) { + var userRewards = {}; + + data.forEach(function(obj) { + userRewards[obj.value] = parseInt(obj.score, 10); + }); + + rewards = rewards.filter(function(reward) { + var claimable = parseInt(reward.claimable, 10); + + if (claimable === 0) { + return true; + } + + return (userRewards[reward.id] > reward.claimable) ? false : true; + }); + + callback(false, rewards); + }); +} + +function getRewardDataByIDs(ids, callback) { + db.getObjects(ids.map(function(id) { + return 'rewards:id:' + id; + }), callback); +} + +function getRewardsByRewardData(rewards, callback) { + db.getObjects(rewards.map(function(reward) { + return 'rewards:id:' + reward.id + ':rewards'; + }), callback); +} + +function checkCondition(reward, method, callback) { + method(function(err, value) { + plugins.fireHook('filter:rewards.checkConditional:' + reward.conditional, {left: value, right: reward.value}, function(err, bool) { + callback(bool); + }); + }); +} + +function giveRewards(uid, rewards, callback) { + getRewardsByRewardData(rewards, function(err, rewardData) { + async.each(rewards, function(reward, next) { + plugins.fireHook('action:rewards.award:' + reward.rid, {uid: uid, reward: rewardData[rewards.indexOf(reward)]}); + db.sortedSetIncrBy('uid:' + uid + ':rewards', 1, reward.id, next); + }, callback); + }); +} + + +module.exports = rewards; \ No newline at end of file diff --git a/src/routes/admin.js b/src/routes/admin.js index 1b9efcef58..5175059493 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -67,6 +67,7 @@ function addRoutes(router, middleware, controllers) { router.get('/extend/plugins', controllers.admin.plugins.get); router.get('/extend/widgets', controllers.admin.extend.widgets); + router.get('/extend/rewards', controllers.admin.extend.rewards); router.get('/advanced/database', controllers.admin.database.get); router.get('/advanced/events', controllers.admin.events.get); diff --git a/src/routes/api.js b/src/routes/api.js index 47632ccce7..ef1c297e49 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -16,6 +16,7 @@ module.exports = function(app, middleware, controllers) { router.get('/widgets/render', controllers.api.renderWidgets); router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.accounts.getUserByUID); + router.get('/post/:pid', controllers.posts.getPost); router.get('/get_templates_listing', templatesController.getTemplatesListing); router.get('/categories/:cid/moderators', getModerators); router.get('/recent/posts/:term?', getRecentPosts); diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 649d6d87dd..5b8a3ffe5f 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -4,11 +4,12 @@ var passport = require('passport'), passportLocal = require('passport-local').Strategy, nconf = require('nconf'), - Password = require('../password'), winston = require('winston'), async = require('async'), + validator = require('validator'), express = require('express'), + Password = require('../password'), meta = require('../meta'), user = require('../user'), plugins = require('../plugins'), @@ -131,7 +132,9 @@ req.session.returnTo = req.body.returnTo; } - if (req.body.username && utils.isEmailValid(req.body.username)) { + var loginWith = meta.config.allowLoginWith || 'username-email'; + + if (req.body.username && utils.isEmailValid(req.body.username) && loginWith.indexOf('email') !== -1) { user.getUsernameByEmail(req.body.username, function(err, username) { if (err) { return next(err); @@ -139,8 +142,10 @@ req.body.username = username ? username : req.body.username; continueLogin(req, res, next); }); - } else { + } else if (loginWith.indexOf('username') !== -1 && !validator.isEmail(req.body.username)) { continueLogin(req, res, next); + } else { + res.status(500).send('[[error:wrong-login-type-' + loginWith + ']]'); } } diff --git a/src/routes/index.js b/src/routes/index.js index 94767dcc36..6bb83ee556 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -92,7 +92,7 @@ function userRoutes(app, middleware, controllers) { } function groupRoutes(app, middleware, controllers) { - var middlewares = [middleware.checkGlobalPrivacySettings]; + var middlewares = [middleware.checkGlobalPrivacySettings, middleware.exposeGroupName]; setupPageRoute(app, '/groups', middleware, middlewares, controllers.groups.list); setupPageRoute(app, '/groups/:slug', middleware, middlewares, controllers.groups.details); diff --git a/src/search.js b/src/search.js index 5cab307a98..190a764bf2 100644 --- a/src/search.js +++ b/src/search.js @@ -365,47 +365,31 @@ function sortPosts(posts, data) { if (isNumeric) { if (data.sortDirection === 'desc') { - sortDescendingNumeric(posts, fields); + posts.sort(function(p1, p2) { + return p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]]; + }); } else { - sortAscendingNumeric(posts, fields); + posts.sort(function(p1, p2) { + return p1[fields[0]][fields[1]] - p2[fields[0]][fields[1]]; + }); } } else { if (data.sortDirection === 'desc') { - sortDescendingAlpha(posts, fields); + posts.sort(function(p1, p2) { + if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) return -1; + if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) return 1; + return 0; + }); } else { - sortAscendingAlpha(posts, fields); + posts.sort(function(p1, p2) { + if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) return -1; + if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) return 1; + return 0; + }); } } } -function sortAscendingNumeric(posts, fields) { - posts.sort(function(p1, p2) { - return p1[fields[0]][fields[1]] - p2[fields[0]][fields[1]]; - }); -} - -function sortDescendingNumeric(posts, fields) { - posts.sort(function(p1, p2) { - return p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]]; - }); -} - -function sortAscendingAlpha(posts, fields) { - posts.sort(function(p1, p2) { - if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) return -1; - if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) return 1; - return 0; - }); -} - -function sortDescendingAlpha(posts, fields) { - posts.sort(function(p1, p2) { - if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) return -1; - if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) return 1; - return 0; - }); -} - function getSearchCategories(data, callback) { if (!Array.isArray(data.categories) || !data.categories.length || data.categories.indexOf('all') !== -1) { return callback(null, []); diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index a770af5c53..30782f4d6e 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -25,6 +25,7 @@ var async = require('async'), categories: require('./admin/categories'), groups: require('./admin/groups'), tags: require('./admin/tags'), + rewards: require('./admin/rewards'), themes: {}, plugins: {}, widgets: {}, @@ -109,6 +110,20 @@ SocketAdmin.plugins.toggleInstall = function(socket, data, callback) { plugins.toggleInstall(data.id, data.version, callback); }; +SocketAdmin.plugins.getActive = function(socket, data, callback) { + plugins.getActive(callback); +}; + +SocketAdmin.plugins.orderActivePlugins = function(socket, data, callback) { + async.each(data, function(plugin, next) { + if (plugin && plugin.name) { + db.sortedSetAdd('plugins:active', plugin.order || 0, plugin.name, next); + } else { + next(); + } + }, callback); +}; + SocketAdmin.plugins.upgrade = function(socket, data, callback) { plugins.upgrade(data.id, data.version, callback); }; @@ -311,14 +326,24 @@ SocketAdmin.dismissAllFlags = function(socket, data, callback) { posts.dismissAllFlags(callback); }; -SocketAdmin.getMoreFlags = function(socket, after, callback) { - if (!parseInt(after, 10)) { +SocketAdmin.getMoreFlags = function(socket, data, callback) { + if (!data || !parseInt(data.after, 10)) { return callback('[[error:invalid-data]]'); } - after = parseInt(after, 10); - posts.getFlags(socket.uid, after, after + 19, function(err, posts) { - callback(err, {posts: posts, next: after + 20}); - }); + var sortBy = data.sortBy || 'count'; + var byUsername = data.byUsername || ''; + var start = parseInt(data.after, 10); + var end = start + 19; + if (byUsername) { + posts.getUserFlags(byUsername, sortBy, socket.uid, start, end, function(err, posts) { + callback(err, {posts: posts, next: end + 1}); + }); + } else { + var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'; + posts.getFlags(set, socket.uid, start, end, function(err, posts) { + callback(err, {posts: posts, next: end + 1}); + }); + } }; SocketAdmin.takeHeapSnapshot = function(socket, data, callback) { diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 9a9ef0ea7e..55d8300819 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -113,12 +113,18 @@ Categories.setGroupPrivilege = function(socket, data, callback) { Categories.groupsList = function(socket, cid, callback) { groups.list({ expand: false, + isAdmin: true, showSystemGroups: true }, function(err, data) { if(err) { return callback(err); } + // Remove privilege groups + data = data.filter(function(groupObj) { + return groupObj.name.indexOf(':privileges:') === -1; + }); + async.map(data, function(groupObj, next) { privileges.categories.groupPrivileges(cid, groupObj.name, function(err, privileges) { if(err) { diff --git a/src/socket.io/admin/rewards.js b/src/socket.io/admin/rewards.js new file mode 100644 index 0000000000..b130a25455 --- /dev/null +++ b/src/socket.io/admin/rewards.js @@ -0,0 +1,15 @@ +"use strict"; + +var rewardsAdmin = require('../../rewards/admin'), + SocketRewards = {}; + +SocketRewards.save = function(socket, data, callback) { + rewardsAdmin.save(data, callback); +}; + +SocketRewards.delete = function(socket, data, callback) { + rewardsAdmin.delete(data, callback); +}; + + +module.exports = SocketRewards; \ No newline at end of file diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index ba35e128c4..bb814e6006 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -101,6 +101,34 @@ SocketGroups.reject = function(socket, data, callback) { }); }; +SocketGroups.acceptInvite = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.isInvited(socket.uid, data.groupName, function(err, invited) { + if (!invited) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.acceptMembership(data.groupName, socket.uid, callback); + }); +}; + +SocketGroups.rejectInvite = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.isInvited(socket.uid, data.groupName, function(err, invited) { + if (!invited) { + return callback(new Error('[[error:no-privileges]]')); + } + + groups.rejectMembership(data.groupName, socket.uid, callback); + }); +}; + SocketGroups.update = function(socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 98d1d6e2e8..aed324d520 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -278,9 +278,10 @@ Sockets.getUsersInRoom = function (uid, roomName, callback) { var uids = Sockets.getUidsInRoom(roomName); var total = uids.length; uids = uids.slice(0, 9); - if (uid) { + if (uid && uids.indexOf(uid.toString()) === -1) { uids = [uid].concat(uids); } + if (!uids.length) { return callback(null, {users: [], total: 0 , room: roomName}); } diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 71a44f36bd..306d26d579 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -12,7 +12,6 @@ var nconf = require('nconf'), meta = require('../meta'), Messaging = require('../messaging'), user = require('../user'), - notifications = require('../notifications'), plugins = require('../plugins'), utils = require('../../public/src/utils'), privileges = require('../privileges'), @@ -23,7 +22,6 @@ var nconf = require('nconf'), SocketModules = { composer: {}, chats: {}, - notifications: {}, sounds: {}, settings: {} }; @@ -116,6 +114,16 @@ SocketModules.composer.stopNotifyTyping = function(socket, data) { server.in('topic_' + data.tid).emit('event:topic.stopNotifyTyping', data); }; +SocketModules.composer.getFormattingOptions = function(socket, data, callback) { + plugins.fireHook('filter:composer.formatting', { + options: [ + // { className: 'fa fa-bold' } Just an example of what needs to be set via plugins + ] + }, function(err, payload) { + callback(err, payload.options); + }); +}; + /* Chat */ SocketModules.chats.get = function(socket, data, callback) { @@ -246,14 +254,6 @@ SocketModules.chats.getRecentChats = function(socket, data, callback) { Messaging.getRecentChats(socket.uid, start, end, callback); }; -/* Notifications */ -SocketModules.notifications.markRead = function(socket, nid) { - notifications.markRead(nid, socket.uid); -}; - -SocketModules.notifications.markAllRead = function(socket, data, callback) { - notifications.markAllRead(socket.uid, callback); -}; /* Sounds */ SocketModules.sounds.getSounds = function(socket, data, callback) { diff --git a/src/socket.io/notifications.js b/src/socket.io/notifications.js index 657e46db52..cb93e1ca45 100644 --- a/src/socket.io/notifications.js +++ b/src/socket.io/notifications.js @@ -1,7 +1,7 @@ "use strict"; var user = require('../user'), - + notifications = require('../notifications'), SocketNotifs = {}; SocketNotifs.get = function(socket, data, callback) { @@ -20,4 +20,16 @@ SocketNotifs.deleteAll = function(socket, data, callback) { user.notifications.deleteAll(socket.uid, callback); }; +SocketNotifs.markRead = function(socket, nid, callback) { + notifications.markRead(nid, socket.uid, callback); +}; + +SocketNotifs.markUnread = function(socket, nid, callback) { + notifications.markUnread(nid, socket.uid, callback); +}; + +SocketNotifs.markAllRead = function(socket, data, callback) { + notifications.markAllRead(socket.uid, callback); +}; + module.exports = SocketNotifs; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index a2de8ca5d6..0b5ab9b418 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -422,14 +422,14 @@ SocketPosts.flag = function(socket, pid, callback) { return next(new Error('[[error:not-enough-reputation-to-flag]]')); } userName = userData.username; - posts.getPostFields(pid, ['tid', 'uid', 'content', 'deleted'], next); + posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next); }, function(postData, next) { if (parseInt(postData.deleted, 10) === 1) { return next(new Error('[[error:post-deleted]]')); } post = postData; - posts.flag(pid, next); + posts.flag(post, socket.uid, next); }, function(next) { topics.getTopicFields(post.tid, ['title', 'cid'], next); @@ -462,14 +462,7 @@ SocketPosts.flag = function(socket, pid, callback) { } notifications.push(notification, results.admins.concat(results.moderators), next); }); - }, - function(next) { - if (!parseInt(post.uid, 10)) { - return next(); - } - - db.setAdd('uid:' + post.uid + ':flagged_by', socket.uid, next); - } + } ], callback); }; diff --git a/src/socket.io/user.js b/src/socket.io/user.js index fd23405758..15aa0e2ba6 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -294,7 +294,6 @@ SocketUser.follow = function(socket, data, callback) { notifications.create({ bodyShort: '[[notifications:user_started_following_you, ' + userData.username + ']]', - path: nconf.get('relative_path') + '/user/' + userData.userslug, nid: 'follow:' + data.uid + ':uid:' + socket.uid, from: socket.uid }, function(err, notification) { diff --git a/src/topics/create.js b/src/topics/create.js index 2eb882da80..1e9336fedc 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -243,7 +243,7 @@ module.exports = function(Topics) { posts.getUserInfoForPosts([postData.uid], uid, next); }, topicInfo: function(next) { - Topics.getTopicFields(tid, ['tid', 'title', 'slug', 'cid'], next); + Topics.getTopicFields(tid, ['tid', 'title', 'slug', 'cid', 'postcount'], next); }, settings: function(next) { user.getSettings(uid, next); diff --git a/src/topics/fork.js b/src/topics/fork.js index 4cd5e0c53e..3302797f38 100644 --- a/src/topics/fork.js +++ b/src/topics/fork.js @@ -41,6 +41,10 @@ module.exports = function(Topics) { posts.getCidByPid(mainPid, callback); } }, function(err, results) { + if (err) { + return callback(err); + } + Topics.create({uid: results.postData.uid, title: title, cid: results.cid}, function(err, tid) { if (err) { return callback(err); diff --git a/src/upgrade.js b/src/upgrade.js index 52347bcb40..13484b124e 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -21,7 +21,7 @@ var db = require('./database'), schemaDate, thisSchemaDate, // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema - latestSchema = Date.UTC(2015, 1, 8); + latestSchema = Date.UTC(2015, 1, 24, 1); Upgrade.check = function(callback) { db.get('schemaDate', function(err, value) { @@ -250,11 +250,24 @@ Upgrade.upgrade = function(callback) { }, next); } - Groups.list({showSystemGroups: true}, function(err, groups) { + async.waterfall([ + async.apply(db.getSetMembers, 'groups'), + function(groups, next) { + async.filter(groups, function(group, next) { + db.getObjectField('group:' + group, 'hidden', function(err, hidden) { + next(!parseInt(hidden, 10)); + }, next); + }, function(groups) { + next(null, groups); + }); + } + ], function(err, groups) { if (err) { return next(err); } + groups.push('administrators', 'registered-users'); + async.eachLimit(cids, 50, function(cid, next) { upgradePrivileges(cid, groups, next); }, next); @@ -445,7 +458,7 @@ Upgrade.upgrade = function(callback) { if (setting.dailyDigestFreq !== 'off') { db.sortedSetAdd('digest:' + setting.dailyDigestFreq + ':uids', now, setting.uid, next); } else { - next(false); + setImmediate(next); } }, function(err) { if (err) { @@ -827,10 +840,111 @@ Upgrade.upgrade = function(callback) { winston.info('[2015/02/08] Clearing reset tokens skipped'); next(); } + }, + function(next) { + thisSchemaDate = Date.UTC(2015, 1, 17); + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2015/02/17] renaming home.tpl to categories.tpl'); + + db.rename('widgets:home.tpl', 'widgets:categories.tpl', function(err) { + if (err) { + return next(err); + } + + winston.info('[2015/02/17] renaming home.tpl to categories.tpl done'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2015/02/17] renaming home.tpl to categories.tpl skipped'); + next(); + } + }, + function(next) { + thisSchemaDate = Date.UTC(2015, 1, 23); + if (schemaDate < thisSchemaDate) { + db.setAdd('plugins:active', 'nodebb-rewards-essentials', function(err) { + winston.info('[2015/2/23] Activating NodeBB Essential Rewards'); + Plugins.reload(function() { + if (err) { + next(err); + } else { + Upgrade.update(thisSchemaDate, next); + } + }); + }); + } else { + winston.info('[2015/2/23] Activating NodeBB Essential Rewards - skipped'); + next(); + } + }, + function(next) { + thisSchemaDate = Date.UTC(2015, 1, 24); + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2015/02/24] Upgrading plugins:active to sorted set'); + + db.getSetMembers('plugins:active', function(err, activePlugins) { + if (err) { + return next(err); + } + if (!Array.isArray(activePlugins) || !activePlugins.length) { + winston.info('[2015/02/24] Upgrading plugins:active to sorted set done'); + Upgrade.update(thisSchemaDate, next); + } + + db.delete('plugins:active', function(err) { + if (err) { + return next(err); + } + var order = -1; + async.eachSeries(activePlugins, function(plugin, next) { + ++order; + db.sortedSetAdd('plugins:active', order, plugin, next); + }, function(err) { + if (err) { + return next(err); + } + winston.info('[2015/02/24] Upgrading plugins:active to sorted set done'); + Upgrade.update(thisSchemaDate, next); + }); + }); + }); + } else { + winston.info('[2015/02/24] Upgrading plugins:active to sorted set skipped'); + next(); + } + }, + function(next) { + thisSchemaDate = Date.UTC(2015, 1, 24, 1); + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2015/02/24] Upgrading privilege groups to system groups'); + + var isPrivilegeGroup = /^cid:\d+:privileges:[\w:]+$/; + db.getSortedSetRange('groups:createtime', 0, -1, function (err, groupNames) { + groupNames = groupNames.filter(function(name) { + return isPrivilegeGroup.test(name); + }); + + async.eachLimit(groupNames, 5, function(groupName, next) { + db.setObjectField('group:' + groupName, 'system', '1', next); + }, function(err) { + if (err) { + return next(err); + } + winston.info('[2015/02/24] Upgrading privilege groups to system groups done'); + Upgrade.update(thisSchemaDate, next); + }) + }); + } else { + winston.info('[2015/02/24] Upgrading privilege groups to system groups skipped'); + next(); + } } // Add new schema updates here - // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 22!!! + // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!! ], function(err) { if (!err) { if(updatesMade) { diff --git a/src/user/delete.js b/src/user/delete.js index 3440e810a4..1c1e9c2974 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -80,7 +80,7 @@ module.exports = function(User) { 'uid:' + uid + ':topics', 'uid:' + uid + ':posts', 'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread', 'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote', - 'uid:' + uid + ':ignored:cids' + 'uid:' + uid + ':ignored:cids', 'uid:' + uid + ':flag:pids' ]; db.deleteAll(keys, next); }, diff --git a/src/user/notifications.js b/src/user/notifications.js index ccadf4f999..706c227097 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -76,7 +76,7 @@ var async = require('async'), deletedNids.push(nids[index]); } else { notification.read = read; - notification.readClass = !notification.read ? 'label-warning' : ''; + notification.readClass = !notification.read ? 'unread' : ''; } }); @@ -124,6 +124,13 @@ var async = require('async'), } notification.path = pidToPaths[notification.pid] || notification.path || ''; + + if (notification.nid.startsWith('chat')) { + notification.path = nconf.get('relative_path') + '/chats/' + notification.user.userslug; + } else if (notification.nid.startsWith('follow')) { + notification.path = nconf.get('relative_path') + '/user/' + notification.user.userslug; + } + notification.datetimeISO = utils.toISOString(notification.datetime); return notification; }); diff --git a/src/user/reset.js b/src/user/reset.js index 60ff820dbf..7434c4fc30 100644 --- a/src/user/reset.js +++ b/src/user/reset.js @@ -10,92 +10,109 @@ var async = require('async'), db = require('../database'), meta = require('../meta'), - events = require('../events'), emailer = require('../emailer'); (function(UserReset) { - UserReset.validate = function(code, callback) { - db.getObjectField('reset:uid', code, function(err, uid) { - if (err || !uid) { - return callback(err, false); - } + var twoHours = 7200000; - db.sortedSetScore('reset:issueDate', code, function(err, issueDate) { - // db.getObjectField('reset:expiry', code, function(err, expiry) { - if (err) { - return callback(err); + UserReset.validate = function(code, callback) { + async.waterfall([ + function(next) { + db.getObjectField('reset:uid', code, next); + }, + function(uid, next) { + if (!uid) { + return callback(null, false); } - - callback(null, parseInt(issueDate, 10) > (Date.now() - (1000*60*120))); - }); - }); + db.sortedSetScore('reset:issueDate', code, next); + }, + function(issueDate, next) { + next(null, parseInt(issueDate, 10) > Date.now() - twoHours); + } + ], callback); }; UserReset.send = function(email, callback) { - user.getUidByEmail(email, function(err, uid) { - if (err || !uid) { - return callback(err || new Error('[[error:invalid-email]]')); - } - - var reset_code = utils.generateUUID(); - db.setObjectField('reset:uid', reset_code, uid); - db.sortedSetAdd('reset:issueDate', Date.now(), reset_code); - - var reset_link = nconf.get('url') + '/reset/' + reset_code; + var reset_code = utils.generateUUID(); + var uid; + async.waterfall([ + function(next) { + user.getUidByEmail(email, next); + }, + function(_uid, next) { + if (!_uid) { + return next(new Error('[[error:invalid-email]]')); + } - translator.translate('[[email:password-reset-requested, ' + (meta.config.title || 'NodeBB') + ']]', meta.config.defaultLang, function(subject) { + uid = _uid; + async.parallel([ + async.apply(db.setObjectField, 'reset:uid', reset_code, uid), + async.apply(db.sortedSetAdd, 'reset:issueDate', Date.now(), reset_code) + ], next); + }, + function(results, next) { + translator.translate('[[email:password-reset-requested, ' + (meta.config.title || 'NodeBB') + ']]', meta.config.defaultLang, function(subject) { + next(null, subject); + }); + }, + function(subject, next) { + var reset_link = nconf.get('url') + '/reset/' + reset_code; emailer.send('reset', uid, { site_title: (meta.config.title || 'NodeBB'), reset_link: reset_link, subject: subject, template: 'reset', uid: uid - }); - callback(); - }); - }); + }, next); + }, + function(next) { + next(null, reset_code); + } + ], callback); }; UserReset.commit = function(code, password, callback) { - UserReset.validate(code, function(err, validated) { - if(err) { - return callback(err); - } - - if (!validated) { - return callback(new Error('[[error:reset-code-not-valid]]')); - } - - db.getObjectField('reset:uid', code, function(err, uid) { - if (err) { - return callback(err); + var uid; + async.waterfall([ + function(next) { + UserReset.validate(code, next); + }, + function(validated, next) { + if (!validated) { + return next(new Error('[[error:reset-code-not-valid]]')); + } + db.getObjectField('reset:uid', code, next); + }, + function(_uid, next) { + uid = _uid; + if (!uid) { + return next(new Error('[[error:reset-code-not-valid]]')); } - user.hashPassword(password, function(err, hash) { - if (err) { - return callback(err); - } - user.setUserField(uid, 'password', hash); - - db.deleteObjectField('reset:uid', code); - db.sortedSetRemove('reset:issueDate', code); - - user.auth.resetLockout(uid, callback); - }); - }); - }); + user.hashPassword(password, next); + }, + function(hash, next) { + async.parallel([ + async.apply(user.setUserField, uid, 'password', hash), + async.apply(db.deleteObjectField, 'reset:uid', code), + async.apply(db.sortedSetRemove, 'reset:issueDate', code), + async.apply(user.auth.resetLockout, uid) + ], next); + } + ], callback); }; UserReset.clean = function(callback) { - // Locate all codes that have expired, and remove them from the set/hash async.waterfall([ - async.apply(db.getSortedSetRangeByScore, 'reset:issueDate', 0, -1, -1, +new Date()-(1000*60*120)), + async.apply(db.getSortedSetRangeByScore, 'reset:issueDate', 0, -1, 0, Date.now() - twoHours), function(tokens, next) { - if (!tokens.length) { return next(); } + if (!tokens.length) { + return next(); + } winston.verbose('[UserReset.clean] Removing ' + tokens.length + ' reset tokens from database'); async.parallel([ - async.apply(db.deleteObjectField, 'reset:uid', tokens), + async.apply(db.deleteObjectFields, 'reset:uid', tokens), async.apply(db.sortedSetRemove, 'reset:issueDate', tokens) ], next); } diff --git a/src/user/search.js b/src/user/search.js index 20bbec2258..4a26940416 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -10,7 +10,7 @@ var async = require('async'), module.exports = function(User) { User.search = function(data, callback) { - var query = data.query; + var query = data.query || ''; var searchBy = data.searchBy || ['username']; var startsWith = data.hasOwnProperty('startsWith') ? data.startsWith : true; var page = data.page || 1; diff --git a/src/views/admin/extend/plugins.tpl b/src/views/admin/extend/plugins.tpl index b44c2461bc..2fdccebfdd 100644 --- a/src/views/admin/extend/plugins.tpl +++ b/src/views/admin/extend/plugins.tpl @@ -83,7 +83,8 @@
    Plugin Search
    - +
    +
    @@ -96,4 +97,30 @@ + + + + + + + diff --git a/src/views/admin/extend/rewards.tpl b/src/views/admin/extend/rewards.tpl new file mode 100644 index 0000000000..29bf0b8288 --- /dev/null +++ b/src/views/admin/extend/rewards.tpl @@ -0,0 +1,80 @@ +
    +
    +
    +
    Rewards
    +
    +
      + +
    • +
      +
      +
      + +
      +
      +
      + + +
      +
      +
      + +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      + + Enter 0 for infinite +
      +
      +
      + + + + + + +
      +
      +
      +
    • + +
    + + + + +
    +
    +
    + +
    +
    +
    Rewards Control
    +
    + + +
    +
    +
    +
    \ No newline at end of file diff --git a/src/views/admin/header.tpl b/src/views/admin/header.tpl index 9bd680936a..4193857410 100644 --- a/src/views/admin/header.tpl +++ b/src/views/admin/header.tpl @@ -16,91 +16,100 @@ - - - - - - - - - - - - - - + + + + - - - - - + + + -
    -